1183 lines
44 KiB
PHP
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();
|
|
}
|
|
}
|
|
|
|
|