Files
BE_CPONE/application/models/ScreenerModel.php
2026-04-27 10:26:26 +07:00

1183 lines
44 KiB
PHP

<?php
/**
* Screener Model
*
* Handle all database operations for screener tracking system
*/
class ScreenerModel extends CI_Model
{
function __construct()
{
parent::__construct();
// Use main database
$this->db = $this->load->database('screener', TRUE);
}
// ============================================
// TEMPLATE METHODS
// ============================================
public function get_template($template_id)
{
$query = $this->db->get_where('screener_templates', array(
'template_id' => $template_id,
'is_active' => 'Y'
));
$template = $query->row_array();
// Parse JSON configs if they exist
if ($template) {
if (!empty($template['scoring_config_json'])) {
$template['scoring_config'] = json_decode($template['scoring_config_json'], true);
}
if (!empty($template['target_config_json'])) {
$template['target_config'] = json_decode($template['target_config_json'], true);
}
if (!empty($template['filters_json'])) {
$template['filters'] = json_decode($template['filters_json'], true);
}
}
return $template;
}
public function get_template_by_slug($template_slug)
{
$query = $this->db->get_where('screener_templates', array(
'template_slug' => $template_slug,
'is_active' => 'Y'
));
$template = $query->row_array();
// Parse JSON configs if they exist
if ($template) {
if (!empty($template['scoring_config_json'])) {
$template['scoring_config'] = json_decode($template['scoring_config_json'], true);
}
if (!empty($template['target_config_json'])) {
$template['target_config'] = json_decode($template['target_config_json'], true);
}
if (!empty($template['filters_json'])) {
$template['filters'] = json_decode($template['filters_json'], true);
}
}
return $template;
}
public function get_all_templates()
{
$this->db->where('is_active', 'Y');
$this->db->order_by('template_name', 'ASC');
$query = $this->db->get('screener_templates');
return $query->result_array();
}
// ============================================
// SCREENER RUN METHODS
// ============================================
public function save_screener_run($data)
{
$this->db->insert('screener_runs', $data);
return $this->db->insert_id();
}
public function get_screener_run($run_id)
{
$query = $this->db->get_where('screener_runs', array('run_id' => $run_id));
return $query->row_array();
}
public function get_recent_runs($template_id = null, $limit = 10)
{
if ($template_id) {
$this->db->where('template_id', $template_id);
}
$this->db->order_by('run_datetime', 'DESC');
$this->db->limit($limit);
$query = $this->db->get('screener_runs');
return $query->result_array();
}
// ============================================
// SCREENER RESULT METHODS
// ============================================
public function save_screener_result($data)
{
// Use insert ignore to avoid duplicates
$sql = "INSERT IGNORE INTO screener_results (
screener_runs_run_id, symbol, company_name, company_id, entry_price, entry_date, entry_datetime,
metric_1_value, metric_1_name, metric_2_value, metric_2_name,
metric_3_value, metric_3_name, metric_4_value, metric_4_name,
metric_5_value, metric_5_name, metric_6_value, metric_6_name,
metric_7_value, metric_7_name, metric_8_value, metric_8_name,
metric_9_value, metric_9_name, metric_10_value, metric_10_name,
metrics_json, is_scored, is_tracked
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
$params = array(
$data['screener_runs_run_id'],
$data['symbol'],
$data['company_name'],
$data['company_id'],
$data['entry_price'],
$data['entry_date'],
$data['entry_datetime'],
$data['metric_1_value'] ?? null,
$data['metric_1_name'] ?? null,
$data['metric_2_value'] ?? null,
$data['metric_2_name'] ?? null,
$data['metric_3_value'] ?? null,
$data['metric_3_name'] ?? null,
$data['metric_4_value'] ?? null,
$data['metric_4_name'] ?? null,
$data['metric_5_value'] ?? null,
$data['metric_5_name'] ?? null,
$data['metric_6_value'] ?? null,
$data['metric_6_name'] ?? null,
$data['metric_7_value'] ?? null,
$data['metric_7_name'] ?? null,
$data['metric_8_value'] ?? null,
$data['metric_8_name'] ?? null,
$data['metric_9_value'] ?? null,
$data['metric_9_name'] ?? null,
$data['metric_10_value'] ?? null,
$data['metric_10_name'] ?? null,
$data['metrics_json'],
$data['is_scored'],
$data['is_tracked']
);
return $this->db->query($sql, $params);
}
public function get_result($result_id)
{
$sql = "SELECT sr.*, st.template_id, st.template_name, st.strategy_type,
srr.screener_templates_template_id
FROM screener_results sr
JOIN screener_runs srr ON sr.screener_runs_run_id = srr.run_id
JOIN screener_templates st ON srr.screener_templates_template_id = st.template_id
WHERE sr.result_id = ?";
$query = $this->db->query($sql, array($result_id));
return $query->row_array();
}
public function get_unscored_results($run_id)
{
$sql = "SELECT sr.*, st.template_id, st.strategy_type,
srr.screener_templates_template_id
FROM screener_results sr
JOIN screener_runs srr ON sr.screener_runs_run_id = srr.run_id
JOIN screener_templates st ON srr.screener_templates_template_id = st.template_id
WHERE sr.screener_runs_run_id = ? AND sr.is_scored = 'N'
ORDER BY sr.symbol";
$query = $this->db->query($sql, array($run_id));
return $query->result_array();
}
public function mark_as_scored($result_id)
{
return $this->db->update('screener_results',
array('is_scored' => 'Y'),
array('result_id' => $result_id)
);
}
public function mark_as_tracked($result_id)
{
return $this->db->update('screener_results',
array('is_tracked' => 'Y'),
array('result_id' => $result_id)
);
}
// ============================================
// SCORE METHODS
// ============================================
public function save_score($data)
{
$this->db->insert('screener_scores', $data);
return $this->db->insert_id();
}
public function get_score($score_id)
{
$query = $this->db->get_where('screener_scores', array('score_id' => $score_id));
return $query->row_array();
}
public function get_score_by_result($result_id)
{
$query = $this->db->get_where('screener_scores', array('screener_results_result_id' => $result_id));
return $query->row_array();
}
public function get_high_scores($run_id, $min_score = 10)
{
$sql = "SELECT ss.*, sr.symbol, sr.company_name, sr.entry_price
FROM screener_scores ss
JOIN screener_results sr ON ss.screener_results_result_id = sr.result_id
WHERE ss.screener_runs_run_id = ? AND ss.total_score >= ?
ORDER BY ss.total_score DESC";
$query = $this->db->query($sql, array($run_id, $min_score));
return $query->result_array();
}
/**
* Build filter JSON for Stockbit API from database
*
* @param int $template_id
* @return array - Array of filter objects for Stockbit API
*/
public function build_filters_from_db($template_id)
{
$sql = "SELECT
stf.*,
m1.fitem_name as metric1_name,
m2.fitem_name as metric2_name
FROM screener_template_filters stf
JOIN screener_metrics m1 ON stf.screener_metrics_metric1_id = m1.fitem_id
LEFT JOIN screener_metrics m2 ON stf.screener_metrics_metric2_id = m2.fitem_id
WHERE stf.screener_templates_template_id = ? AND stf.is_active = 'Y'
ORDER BY stf.filter_order";
$query = $this->db->query($sql, array($template_id));
$filters_data = $query->result_array();
$filters = array();
foreach ($filters_data as $row) {
$filter = array(
'type' => $row['filter_type']
);
if ($row['filter_type'] == 'basic') {
// Basic filter: metric vs value
$filter['item1'] = intval($row['screener_metrics_metric1_id']);
$filter['item1name'] = $row['metric1_name'];
$filter['operator'] = $row['operator'];
$filter['item2'] = $row['value'];
$filter['multiplier'] = '';
} else {
// Compare filter: metric vs metric
$filter['item1'] = intval($row['screener_metrics_metric1_id']);
$filter['item1name'] = $row['metric1_name'];
$filter['operator'] = $row['operator'];
$filter['item2'] = intval($row['screener_metrics_metric2_id']);
$filter['item2name'] = $row['metric2_name'];
$filter['multiplier'] = $row['multiplier'] ?? '1';
}
$filters[] = $filter;
}
return $filters;
}
/**
* Build sequence string for Stockbit API from filters
*
* @param int $template_id
* @return string - Comma-separated fitem_ids
*/
public function build_sequence_from_db($template_id)
{
$sql = "SELECT
DISTINCT stf.screener_metrics_metric1_id,
stf.screener_metrics_metric2_id
FROM screener_template_filters stf
WHERE stf.screener_templates_template_id = ? AND stf.is_active = 'Y'
ORDER BY stf.filter_order";
$query = $this->db->query($sql, array($template_id));
$rows = $query->result_array();
$metric_ids = array();
foreach ($rows as $row) {
$metric_ids[] = $row['screener_metrics_metric1_id'];
if (!empty($row['screener_metrics_metric2_id'])) {
$metric_ids[] = $row['screener_metrics_metric2_id'];
}
}
// Remove duplicates and return as comma-separated string
$metric_ids = array_unique($metric_ids);
return implode(',', $metric_ids);
}
/**
* Get all metrics (for building UI/admin panel)
*
* @param string $category_filter - Optional category filter
* @return array
*/
public function get_all_metrics($category_filter = null)
{
$this->db->select('*');
$this->db->from('screener_metrics');
$this->db->where('is_active', 'Y');
if ($category_filter) {
$this->db->like('category_path', $category_filter);
}
$this->db->order_by('category_path', 'ASC');
$this->db->order_by('fitem_name', 'ASC');
$query = $this->db->get();
return $query->result_array();
}
/**
* Get template filters for display/editing
*
* @param int $template_id
* @return array
*/
public function get_template_filters($template_id)
{
$sql = "SELECT
stf.*,
m1.fitem_name as metric1_name,
m1.category_path as metric1_category,
m2.fitem_name as metric2_name,
m2.category_path as metric2_category
FROM screener_template_filters stf
JOIN screener_metrics m1 ON stf.screener_metrics_metric1_id = m1.fitem_id
LEFT JOIN screener_metrics m2 ON stf.screener_metrics_metric2_id = m2.fitem_id
WHERE stf.screener_templates_template_id = ?
ORDER BY stf.filter_order";
$query = $this->db->query($sql, array($template_id));
return $query->result_array();
}
/**
* Get scoring configuration from database tables
* Alternative to JSON-based scoring_config
*
* @param int $template_id
* @return array - Scoring config in same format as JSON
*/
public function get_scoring_config_from_db($template_id)
{
// Get categories with their rules
$sql = "SELECT
ssc.category_id,
ssc.category_number,
ssc.category_name,
ssc.max_score,
ssc.description as category_description,
ssr.rule_id,
ssr.metric_key,
ssr.metric_display_name,
ssr.range_condition,
ssr.points,
ssr.description as rule_description,
ssr.rule_order
FROM screener_scoring_categories ssc
LEFT JOIN screener_scoring_rules ssr ON ssc.category_id = ssr.screener_scoring_categories_category_id AND ssr.is_active = 'Y'
WHERE ssc.screener_templates_template_id = ? AND ssc.is_active = 'Y'
ORDER BY ssc.sort_order, ssr.rule_order";
$query = $this->db->query($sql, array($template_id));
$rows = $query->result_array();
if (empty($rows)) {
return null;
}
// Group by categories
$categories = array();
$current_category = null;
foreach ($rows as $row) {
$cat_num = $row['category_number'];
// Initialize category if new
if (!isset($categories[$cat_num])) {
$categories[$cat_num] = array(
'name' => $row['category_name'],
'max' => floatval($row['max_score']),
'description' => $row['category_description'],
'rules' => array()
);
}
// Add rule if exists
if (!empty($row['rule_id'])) {
$categories[$cat_num]['rules'][] = array(
'metric' => $row['metric_key'],
'range' => $row['range_condition'],
'points' => floatval($row['points']),
'description' => $row['rule_description']
);
}
}
// Convert to indexed array
$categories_array = array_values($categories);
return array(
'categories' => $categories_array
);
}
/**
* Calculate score based on template's scoring configuration
*
* @param array $template - Template with scoring_config
* @param array $metrics - Stock metrics from screener result
* @return array - Score breakdown
*/
public function calculate_score_from_template($template, $metrics)
{
$scoring_config = $template['scoring_config'] ?? null;
// If no JSON config, try to get from database tables
if (!$scoring_config || empty($scoring_config['categories'])) {
$scoring_config = $this->get_scoring_config_from_db($template['template_id']);
}
if (!$scoring_config || empty($scoring_config['categories'])) {
// Fallback to default scoring if no config
return $this->calculate_default_score($metrics);
}
$category_scores = array();
$total_score = 0;
$max_possible_score = 0;
$scoring_notes = array();
// Process each category
$category_num = 1;
foreach ($scoring_config['categories'] as $category) {
$category_name = $category['name'];
$category_max = $category['max'];
$category_score = 0;
$category_notes = array();
// Apply rules to calculate category score
foreach ($category['rules'] as $rule) {
$metric_key = $rule['metric'];
$points = $rule['points'];
$range = $rule['range'];
// Try to find metric value
$metric_value = null;
if (isset($metrics[$metric_key])) {
$metric_value = $metrics[$metric_key];
} else {
$metric_value = $this->find_metric_value($metrics, $metric_key);
}
// Check if metric matches the range/condition
$matched = $this->metric_matches_rule($metrics, $metric_key, $range);
if ($matched) {
$category_score += $points;
$category_notes[] = sprintf(
"✓ %s (%s): +%s points",
$metric_key,
$range,
$points
);
} else {
$category_notes[] = sprintf(
"✗ %s (%s): 0 points [value: %s]",
$metric_key,
$range,
$metric_value !== null ? $metric_value : 'NOT FOUND'
);
}
}
// Cap at category max
$category_score = min($category_score, $category_max);
$category_scores['category_' . $category_num] = array(
'name' => $category_name,
'score' => $category_score,
'max' => $category_max,
'notes' => $category_notes
);
$scoring_notes[$category_name] = $category_notes;
$total_score += $category_score;
$max_possible_score += $category_max;
$category_num++;
}
// Calculate decision based on total score
$score_percentage = ($total_score / $max_possible_score) * 100;
$decision = $this->determine_decision($score_percentage, $total_score, $template);
return array(
'category_scores' => $category_scores,
'total_score' => $total_score,
'max_possible_score' => $max_possible_score,
'score_percentage' => $score_percentage,
'decision' => $decision['decision'],
'confidence' => $decision['confidence'],
'suggested_position_size' => $decision['position_size'],
'scoring_notes' => $scoring_notes
);
}
/**
* Check if a metric matches a rule
*
* @param array $metrics - Stock metrics
* @param string $metric_key - Metric identifier
* @param string $range - Rule range/condition
* @return bool
*/
private function metric_matches_rule($metrics, $metric_key, $range)
{
// Try to get metric value - try exact key first, then variations
$value = null;
// Direct match
if (isset($metrics[$metric_key])) {
$value = $metrics[$metric_key];
} else {
// Try to find by partial match or common variations
$value = $this->find_metric_value($metrics, $metric_key);
}
if ($value === null) {
// Metric not found - return false
return false;
}
// Handle different range formats
return $this->check_range($value, $range);
}
/**
* Find metric value by key variations
*/
private function find_metric_value($metrics, $metric_key)
{
// Common metric key mappings
$key_mappings = array(
'net_foreign_buy' => array('foreign_buy', 'net_foreign_buy_sell', 'foreign_flow'),
'foreign_streak' => array('foreign_buy_streak', 'net_foreign_buy_streak'),
'foreign_flow_1m' => array('foreign_1m', 'foreign_flow_1_month', '1m_foreign_flow'),
'bandar_accum_dist' => array('bandar_accum', 'bandar_accumdist', 'bandar_ad'),
'bandar_value_trend' => array('bandar_value', 'bandar_val'),
'1d_return' => array('1_day_return', '1day_return', 'daily_return'),
'1w_return' => array('1_week_return', '1week_return', 'weekly_return'),
'1m_return' => array('1_month_return', '1month_return', 'monthly_return'),
'1d_vol_change' => array('1_day_volume_change', 'volume_change_1d', 'daily_vol_change'),
'price_vs_ma20' => array('price_ma20', 'price_over_ma20'),
'volume_vs_ma20' => array('volume_ma20', 'volume_over_ma20'),
'rs_rating' => array('relative_strength', 'rs_score')
);
// Check if we have a mapping for this key
if (isset($key_mappings[$metric_key])) {
foreach ($key_mappings[$metric_key] as $variation) {
if (isset($metrics[$variation])) {
return $metrics[$variation];
}
}
}
// Try partial match - find any key that contains the metric_key
foreach ($metrics as $key => $val) {
if (strpos($key, $metric_key) !== false || strpos($metric_key, $key) !== false) {
return $val;
}
}
return null;
}
/**
* Check if value matches range condition
*/
private function check_range($value, $range)
{
// Handle qualitative checks
if (in_array(strtolower($range), array('up', 'positive', 'increasing', 'yes', 'true'))) {
return $value > 0;
}
if (in_array(strtolower($range), array('down', 'negative', 'decreasing', 'no', 'false'))) {
return $value < 0;
}
if (strtolower($range) === 'golden') {
// Golden cross means value > 0 (MA crossover)
return $value > 0;
}
if (strtolower($range) === 'clean') {
// Clean pattern - assumed true if we got this far
return true;
}
if (strtolower($range) === 'confirmed') {
// Confirmed breakout - assumed true if value > 0
return $value > 0;
}
// Comparison operators (>=, <=, >, <, =)
if (preg_match('/^(>=|<=|>|<|=)(.+)$/', trim($range), $matches)) {
$operator = $matches[1];
$threshold = $this->parse_threshold($matches[2]);
switch ($operator) {
case '>':
return $value > $threshold;
case '>=':
return $value >= $threshold;
case '<':
return $value < $threshold;
case '<=':
return $value <= $threshold;
case '=':
case '==':
return abs($value - $threshold) < 0.0001; // Float comparison
}
}
// Range check (e.g., "50-100", "3-10%", "0.3-1")
if (preg_match('/^(\d+\.?\d*)-(\d+\.?\d*)/', trim($range), $matches)) {
$min = floatval($matches[1]);
$max = floatval($matches[2]);
return ($value >= $min && $value <= $max);
}
// Special range formats (e.g., "-5to+5%", "10-20%")
if (preg_match('/^(-?\d+\.?\d*)to\+?(-?\d+\.?\d*)/', trim($range), $matches)) {
$min = floatval($matches[1]);
$max = floatval($matches[2]);
return ($value >= $min && $value <= $max);
}
// String matching for qualitative values
if (is_string($value)) {
return strtolower(trim($value)) === strtolower(trim($range));
}
return false;
}
/**
* Parse threshold value (handle M, B, K suffixes)
*/
private function parse_threshold($threshold_str)
{
$threshold_str = trim($threshold_str);
// Remove percentage sign
$threshold_str = str_replace('%', '', $threshold_str);
// Handle M (million), B (billion), K (thousand)
$multiplier = 1;
if (preg_match('/(\d+\.?\d*)([MBK])$/i', $threshold_str, $matches)) {
$number = floatval($matches[1]);
$suffix = strtoupper($matches[2]);
switch ($suffix) {
case 'K':
$multiplier = 1000;
break;
case 'M':
$multiplier = 1000000;
break;
case 'B':
$multiplier = 1000000000;
break;
}
return $number * $multiplier;
}
return floatval($threshold_str);
}
/**
* Determine decision based on score
*/
private function determine_decision($score_percentage, $total_score, $template)
{
$min_score = $template['min_score'] ?? 8;
$max_score = $template['max_score'] ?? 12;
// Decision thresholds
if ($score_percentage >= 90 || $total_score >= ($max_score - 1)) {
return array(
'decision' => 'STRONG_BUY',
'confidence' => 'VERY_HIGH',
'position_size' => 5
);
} elseif ($score_percentage >= 75 || $total_score >= $min_score) {
return array(
'decision' => 'BUY',
'confidence' => 'HIGH',
'position_size' => 3.5
);
} elseif ($score_percentage >= 60) {
return array(
'decision' => 'WATCH',
'confidence' => 'MEDIUM',
'position_size' => 2
);
} else {
return array(
'decision' => 'SKIP',
'confidence' => 'LOW',
'position_size' => 0
);
}
}
/**
* Default scoring fallback (for templates without scoring_config)
*/
private function calculate_default_score($metrics)
{
// Simple default scoring based on common metrics
$total_score = 0;
$max_possible = 12;
// This is a basic fallback - can be expanded
return array(
'category_scores' => array(),
'total_score' => $total_score,
'max_possible_score' => $max_possible,
'score_percentage' => 0,
'decision' => 'SKIP',
'confidence' => 'LOW',
'suggested_position_size' => 0
);
}
// ============================================
// PRICE TRACKING METHODS
// ============================================
public function save_price_tracking($data)
{
// Use replace to update if exists
$sql = "REPLACE INTO screener_price_tracking (
screener_results_result_id, symbol, tracking_date, tracking_datetime,
current_price, open_price, high_price, low_price,
volume, value, frequency,
foreign_buy, foreign_sell, foreign_net,
ma_5, ma_10, ma_20, ma_50,
entry_price, return_amount, return_percentage,
days_since_entry, weeks_since_entry
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
$params = array(
$data['screener_results_result_id'],
$data['symbol'],
$data['tracking_date'],
$data['tracking_datetime'],
$data['current_price'],
$data['open_price'] ?? null,
$data['high_price'] ?? null,
$data['low_price'] ?? null,
$data['volume'] ?? null,
$data['value'] ?? null,
$data['frequency'] ?? null,
$data['foreign_buy'] ?? null,
$data['foreign_sell'] ?? null,
$data['foreign_net'] ?? null,
$data['ma_5'] ?? null,
$data['ma_10'] ?? null,
$data['ma_20'] ?? null,
$data['ma_50'] ?? null,
$data['entry_price'],
$data['return_amount'],
$data['return_percentage'],
$data['days_since_entry'],
$data['weeks_since_entry']
);
return $this->db->query($sql, $params);
}
public function get_latest_price($result_id)
{
$sql = "SELECT * FROM screener_price_tracking
WHERE screener_results_result_id = ?
ORDER BY tracking_date DESC
LIMIT 1";
$query = $this->db->query($sql, array($result_id));
return $query->row_array();
}
public function get_price_history($result_id, $days = 30)
{
$sql = "SELECT * FROM screener_price_tracking
WHERE screener_results_result_id = ?
AND tracking_date >= DATE_SUB(CURDATE(), INTERVAL ? DAY)
ORDER BY tracking_date ASC";
$query = $this->db->query($sql, array($result_id, $days));
return $query->result_array();
}
// ============================================
// PERFORMANCE METHODS
// ============================================
public function create_performance($data)
{
$this->db->insert('screener_performance', $data);
return $this->db->insert_id();
}
public function get_performance($performance_id)
{
$query = $this->db->get_where('screener_performance', array('performance_id' => $performance_id));
return $query->row_array();
}
public function get_performance_by_result($result_id)
{
$query = $this->db->get_where('screener_performance', array('screener_results_result_id' => $result_id));
return $query->row_array();
}
public function update_performance($performance_id, $data)
{
return $this->db->update('screener_performance', $data, array('performance_id' => $performance_id));
}
public function close_performance($performance_id, $data)
{
// Determine outcome
$return_pct = $data['return_percentage'];
if ($return_pct >= 40) {
$outcome = 'HOME_RUN';
$win_loss = 'WIN';
} elseif ($return_pct >= 20) {
$outcome = 'WINNER';
$win_loss = 'WIN';
} elseif ($return_pct >= 10) {
$outcome = 'SMALL_WIN';
$win_loss = 'WIN';
} elseif ($return_pct >= -5) {
$outcome = 'BREAKEVEN';
$win_loss = 'BREAKEVEN';
} else {
$outcome = 'LOSS';
$win_loss = 'LOSS';
}
$data['outcome'] = $outcome;
$data['win_loss'] = $win_loss;
return $this->db->update('screener_performance', $data, array('performance_id' => $performance_id));
}
public function get_active_positions()
{
$sql = "SELECT sp.*,
sr.result_id, sr.symbol, sr.company_name,
sr.entry_date, sr.entry_price,
sp.screener_results_result_id
FROM screener_performance sp
JOIN screener_results sr ON sp.screener_results_result_id = sr.result_id
WHERE sp.is_active = 'Y'
ORDER BY sp.entry_date DESC";
$query = $this->db->query($sql);
return $query->result_array();
}
public function get_active_positions_with_details($filters = array())
{
$sql = "SELECT
sp.performance_id,
sp.symbol,
sp.screener_results_result_id,
sr.result_id,
sr.company_name,
st.template_name,
st.strategy_type,
sp.entry_date,
sp.entry_price,
sp.entry_score,
sp.entry_decision,
sp.days_held,
sp.weeks_held,
sp.target_1_hit,
sp.target_2_hit,
sp.target_3_hit,
sp.max_gain_percentage,
sp.max_loss_percentage,
spt.current_price,
spt.return_percentage,
spt.tracking_date as last_updated,
spt.foreign_net,
ss.total_score,
ss.decision,
ss.confidence
FROM screener_performance sp
JOIN screener_results sr ON sp.screener_results_result_id = sr.result_id
JOIN screener_templates st ON sp.screener_templates_template_id = st.template_id
LEFT JOIN screener_scores ss ON sp.screener_scores_score_id = ss.score_id
LEFT JOIN screener_price_tracking spt ON sp.screener_results_result_id = spt.screener_results_result_id
AND spt.tracking_date = (
SELECT MAX(tracking_date)
FROM screener_price_tracking
WHERE screener_results_result_id = sp.screener_results_result_id
)
WHERE sp.is_active = 'Y'";
if (!empty($filters['template_id'])) {
$sql .= " AND sp.screener_templates_template_id = " . intval($filters['template_id']);
}
if (!empty($filters['min_score'])) {
$sql .= " AND ss.total_score >= " . floatval($filters['min_score']);
}
$sort_by = !empty($filters['sort_by']) ? $filters['sort_by'] : 'return_percentage';
$order = !empty($filters['order']) ? strtoupper($filters['order']) : 'DESC';
$sql .= " ORDER BY spt." . $this->db->escape_str($sort_by) . " " . $order;
$query = $this->db->query($sql);
return $query->result_array();
}
public function get_symbol_performances($symbol)
{
$sql = "SELECT sp.*,
sr.result_id, sr.company_name,
st.template_name,
sp.screener_results_result_id
FROM screener_performance sp
JOIN screener_results sr ON sp.screener_results_result_id = sr.result_id
JOIN screener_templates st ON sp.screener_templates_template_id = st.template_id
WHERE sp.symbol = ?
ORDER BY sp.entry_date DESC";
$query = $this->db->query($sql, array($symbol));
return $query->result_array();
}
public function mark_target_hit($performance_id, $target_num, $date)
{
$field = 'target_' . $target_num . '_hit';
$date_field = 'target_' . $target_num . '_date';
return $this->db->update('screener_performance',
array(
$field => 'Y',
$date_field => $date
),
array('performance_id' => $performance_id)
);
}
// ============================================
// STATISTICS METHODS
// ============================================
public function calculate_statistics($template_id)
{
// Delete existing ALL_TIME stats
$this->db->delete('screener_statistics', array(
'screener_templates_template_id' => $template_id,
'period' => 'ALL_TIME'
));
$sql = "INSERT INTO screener_statistics (
screener_templates_template_id, stat_date, period,
total_trades, closed_trades, open_trades,
winning_trades, losing_trades, breakeven_trades,
win_rate_percentage, loss_rate_percentage,
avg_win_percentage, avg_loss_percentage, avg_return_percentage,
best_trade_percentage, worst_trade_percentage,
avg_days_held, avg_days_winners, avg_days_losers,
target_1_hit_rate, target_2_hit_rate, target_3_hit_rate,
avg_score_winners, avg_score_losers,
calculated_at
)
SELECT
? as template_id,
CURDATE() as stat_date,
'ALL_TIME' as period,
COUNT(*) as total_trades,
SUM(CASE WHEN is_active = 'N' THEN 1 ELSE 0 END) as closed_trades,
SUM(CASE WHEN is_active = 'Y' THEN 1 ELSE 0 END) as open_trades,
SUM(CASE WHEN win_loss = 'WIN' THEN 1 ELSE 0 END) as winning_trades,
SUM(CASE WHEN win_loss = 'LOSS' THEN 1 ELSE 0 END) as losing_trades,
SUM(CASE WHEN win_loss = 'BREAKEVEN' THEN 1 ELSE 0 END) as breakeven_trades,
ROUND(SUM(CASE WHEN win_loss = 'WIN' THEN 1 ELSE 0 END) * 100.0 /
NULLIF(SUM(CASE WHEN is_active = 'N' THEN 1 ELSE 0 END), 0), 2) as win_rate,
ROUND(SUM(CASE WHEN win_loss = 'LOSS' THEN 1 ELSE 0 END) * 100.0 /
NULLIF(SUM(CASE WHEN is_active = 'N' THEN 1 ELSE 0 END), 0), 2) as loss_rate,
ROUND(AVG(CASE WHEN win_loss = 'WIN' THEN return_percentage END), 4) as avg_win,
ROUND(AVG(CASE WHEN win_loss = 'LOSS' THEN return_percentage END), 4) as avg_loss,
ROUND(AVG(CASE WHEN is_active = 'N' THEN return_percentage END), 4) as avg_return,
ROUND(MAX(return_percentage), 4) as best_trade,
ROUND(MIN(return_percentage), 4) as worst_trade,
ROUND(AVG(CASE WHEN is_active = 'N' THEN days_held END), 2) as avg_days_held,
ROUND(AVG(CASE WHEN win_loss = 'WIN' THEN days_held END), 2) as avg_days_winners,
ROUND(AVG(CASE WHEN win_loss = 'LOSS' THEN days_held END), 2) as avg_days_losers,
ROUND(SUM(CASE WHEN target_1_hit = 'Y' THEN 1 ELSE 0 END) * 100.0 /
NULLIF(COUNT(*), 0), 2) as target_1_rate,
ROUND(SUM(CASE WHEN target_2_hit = 'Y' THEN 1 ELSE 0 END) * 100.0 /
NULLIF(COUNT(*), 0), 2) as target_2_rate,
ROUND(SUM(CASE WHEN target_3_hit = 'Y' THEN 1 ELSE 0 END) * 100.0 /
NULLIF(COUNT(*), 0), 2) as target_3_rate,
ROUND(AVG(CASE WHEN win_loss = 'WIN' THEN entry_score END), 2) as avg_score_winners,
ROUND(AVG(CASE WHEN win_loss = 'LOSS' THEN entry_score END), 2) as avg_score_losers,
NOW() as calculated_at
FROM screener_performance
WHERE screener_templates_template_id = ?
HAVING total_trades > 0";
return $this->db->query($sql, array($template_id, $template_id));
}
public function get_statistics($template_id, $period = 'ALL_TIME')
{
$this->db->where('screener_templates_template_id', $template_id);
$this->db->where('period', $period);
$this->db->order_by('stat_date', 'DESC');
$this->db->limit(1);
$query = $this->db->get('screener_statistics');
return $query->row_array();
}
public function get_all_statistics($period = 'ALL_TIME')
{
$sql = "SELECT sst.*, st.template_name, st.strategy_type
FROM screener_statistics sst
JOIN screener_templates st ON sst.screener_templates_template_id = st.template_id
WHERE sst.period = ?
AND sst.stat_date IN (
SELECT MAX(stat_date)
FROM screener_statistics
WHERE screener_templates_template_id = sst.screener_templates_template_id
AND period = ?
)
ORDER BY st.template_name";
$query = $this->db->query($sql, array($period, $period));
return $query->result_array();
}
// ============================================
// ALERT METHODS
// ============================================
public function save_alert($result_id, $alert)
{
$data = array(
'screener_results_result_id' => $result_id,
'symbol' => $alert['symbol'],
'alert_type' => $alert['type'],
'alert_level' => $alert['level'] ?? 'INFO',
'alert_message' => $alert['message'],
'alert_data_json' => json_encode($alert),
'alert_date' => date('Y-m-d'),
'alert_datetime' => date('Y-m-d H:i:s'),
'is_read' => 'N',
'is_acted' => 'N'
);
$this->db->insert('screener_alerts', $data);
return $this->db->insert_id();
}
public function get_unread_alerts($limit = 50)
{
$this->db->where('is_read', 'N');
$this->db->order_by('alert_datetime', 'DESC');
$this->db->limit($limit);
$query = $this->db->get('screener_alerts');
return $query->result_array();
}
public function mark_alert_read($alert_id)
{
return $this->db->update('screener_alerts',
array('is_read' => 'Y'),
array('alert_id' => $alert_id)
);
}
// ============================================
// REPORTING METHODS
// ============================================
public function get_portfolio_summary()
{
$sql = "SELECT
COUNT(*) as total_positions,
SUM(CASE WHEN spt.return_percentage > 0 THEN 1 ELSE 0 END) as winners,
SUM(CASE WHEN spt.return_percentage < 0 THEN 1 ELSE 0 END) as losers,
SUM(CASE WHEN spt.return_percentage = 0 THEN 1 ELSE 0 END) as breakeven,
ROUND(AVG(spt.return_percentage), 2) as avg_return,
ROUND(MAX(spt.return_percentage), 2) as best_return,
ROUND(MIN(spt.return_percentage), 2) as worst_return,
ROUND(AVG(sp.days_held), 1) as avg_days_held
FROM screener_performance sp
JOIN screener_price_tracking spt ON sp.screener_results_result_id = spt.screener_results_result_id
WHERE sp.is_active = 'Y'
AND spt.tracking_date = (
SELECT MAX(tracking_date)
FROM screener_price_tracking
WHERE screener_results_result_id = sp.screener_results_result_id
)";
$query = $this->db->query($sql);
return $query->row_array();
}
public function get_daily_performance_report($date)
{
$sql = "SELECT
sp.symbol,
sp.screener_results_result_id,
sr.result_id,
sr.company_name,
st.template_name,
sp.entry_date,
sp.entry_price,
sp.entry_score,
spt.current_price,
spt.return_percentage,
sp.days_held,
sp.target_1_hit,
sp.target_2_hit,
sp.target_3_hit,
spt.foreign_net
FROM screener_performance sp
JOIN screener_results sr ON sp.screener_results_result_id = sr.result_id
JOIN screener_templates st ON sp.screener_templates_template_id = st.template_id
JOIN screener_price_tracking spt ON sp.screener_results_result_id = spt.screener_results_result_id
WHERE sp.is_active = 'Y'
AND spt.tracking_date = ?
ORDER BY spt.return_percentage DESC";
$query = $this->db->query($sql, array($date));
return $query->result_array();
}
}