load->model('ScreenerModel'); $this->load->helper('url'); // Stockbit API configuration $this->stockbit_base_url = 'https://exodus.stockbit.com'; // Token should be refreshed periodically or stored in config $this->stockbit_token = 'Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjU3MDc0NjI3LTg4MWItNDQzZC04OTcyLTdmMmMzOTNlMzYyOSIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7InVzZSI6ImZhanJpaG0iLCJlbWEiOiJtdXJ0aWZhanJpQGdtYWlsLmNvbSIsImZ1bCI6IkZhanJpIE11cnRpIiwic2VzIjoiYjZGMUFkMjhxeVVGSEhueiIsImR2YyI6IjlhNTZhYTA4ZjdmYTc2NDRiOWY0MTRhMzVhYzk2Y2EyIiwidWlkIjo0NDI2NzMsImNvdSI6IklEIn0sImV4cCI6MTc2Mjk1NzkwNSwiaWF0IjoxNzYyODcxNTA1LCJpc3MiOiJTVE9DS0JJVCIsImp0aSI6IjY0YmIxMDk5LTAzNmYtNGMyYS05YTgwLWQ4M2M4MGFjYzkyNCIsIm5iZiI6MTc2Mjg3MTUwNSwidmVyIjoidjEifQ.vYOcwQhuH5RFMpxRXz1Fb7RgvAsia0mMJ16iUUr2DFKOotMyjLlhpG7y839CtdES_6LwpwR9F-qjvvjQ7ilnPOXHob5o2GJRmdnYFlql72PP_-rQTAEexqKz3ghjHAOHHWdISseXGVZT8lWSjoo_71DPa4mL00Gtvj_FZB_ntetGz2YvrDv80Ew0SevbELUmLzSp4bNoCRZcj7VQfShqmm1w3gyVKUiVLHh1ZqNI-DdpYIDqUFBkt1AXBIQ7g8kornoW8-UgDCj805oRInhe8nWANIUDPo1XFPgmG3rkfq7q5PxPfK4gSoMSxEvfoAc6QoKNoHa_Bf5P3rZ88TOPpQ'; // Replace with actual token management } function sys_ok($data) { echo json_encode(array( 'status' => 'OK', 'data' => $data )); exit; } function sys_error($message) { echo json_encode(array( 'status' => 'ERROR', 'message' => $message )); exit; } /** * Run screener and save results * * POST /screener/screenertracker/run_screener * Body: { * "template_id": 1, * "ihsg_price": 7200.00, * "market_sentiment": "BULLISH", * "notes": "Optional notes" * } */ public function run_screener() { try { // Read JSON body manually for POST requests $json = file_get_contents('php://input'); $prm = json_decode($json, true); // Fallback to regular POST if JSON is empty if (empty($prm)) { $prm = $this->input->post(); } // Also check GET params as fallback if (empty($prm)) { $prm = $this->input->get(); } // Validate required params if (!isset($prm['template_id']) || intval($prm['template_id']) <= 0) { $this->sys_error("Template ID is required"); exit; } $template_id = intval($prm['template_id']); $ihsg_price = isset($prm['ihsg_price']) ? floatval($prm['ihsg_price']) : null; $market_sentiment = isset($prm['market_sentiment']) ? $prm['market_sentiment'] : 'NEUTRAL'; $notes = isset($prm['notes']) ? $prm['notes'] : ''; // Get template configuration $template = $this->ScreenerModel->get_template($template_id); if (!$template) { $this->sys_error("Template not found"); exit; } // Call Stockbit API $screener_data = $this->call_stockbit_screener($template); if (!$screener_data || !isset($screener_data['data'])) { $this->sys_error("Failed to fetch screener results from Stockbit"); exit; } // Save screener run $run_data = array( 'screener_templates_template_id' => $template_id, 'run_date' => date('Y-m-d'), 'run_datetime' => date('Y-m-d H:i:s'), 'total_results' => count($screener_data['data']['calcs']), 'ihsg_price' => $ihsg_price, 'market_sentiment' => $market_sentiment, 'notes' => $notes, 'api_response_json' => json_encode($screener_data) ); $run_id = $this->ScreenerModel->save_screener_run($run_data); if (!$run_id) { $this->sys_error("Failed to save screener run"); exit; } // Save individual results $results_saved = 0; foreach ($screener_data['data']['calcs'] as $stock) { $company = $stock['company']; $results = $stock['results']; // Extract price (usually first result or look for "Price" item) $entry_price = 0; foreach ($results as $result) { if (strpos($result['item'], 'Price') !== false && !strpos($result['item'], 'Returns') && !strpos($result['item'], 'MA')) { $entry_price = floatval($result['raw']); break; } } // Prepare metrics $metrics_data = array(); for ($i = 0; $i < min(10, count($results)); $i++) { $metrics_data['metric_' . ($i + 1) . '_value'] = floatval($results[$i]['raw']); $metrics_data['metric_' . ($i + 1) . '_name'] = $results[$i]['item']; } $result_data = array( 'screener_runs_run_id' => $run_id, 'symbol' => $company['symbol'], 'company_name' => $company['name'], 'company_id' => $company['id'], 'entry_price' => $entry_price, 'entry_date' => date('Y-m-d'), 'entry_datetime' => date('Y-m-d H:i:s'), 'metrics_json' => json_encode($results), 'is_scored' => 'N', 'is_tracked' => 'N' ); $result_data = array_merge($result_data, $metrics_data); if ($this->ScreenerModel->save_screener_result($result_data)) { $results_saved++; } } $response = array( 'run_id' => $run_id, 'template_name' => $template['template_name'], 'total_results' => count($screener_data['data']['calcs']), 'results_saved' => $results_saved, 'run_date' => date('Y-m-d H:i:s'), 'next_step' => 'Call score_results to score these stocks' ); $this->sys_ok($response); } catch (Exception $exc) { $this->sys_error($exc->getMessage()); } } /** * Score screener results based on strategy * * POST /screener/screenertracker/score_results * Body: { * "run_id": 1, * "auto_track_high_scores": true, // Auto track high scores * "min_score_to_track": 10 // Optional: Override template's min_score * } * * Note: If min_score_to_track not provided, uses template's min_score (default: 8.0) */ public function score_results() { try { // Read JSON body manually for POST requests $json = file_get_contents('php://input'); $prm = json_decode($json, true); // Fallback to regular POST if JSON is empty if (empty($prm)) { $prm = $this->input->post(); } // Also check GET params as fallback if (empty($prm)) { $prm = $this->input->get(); } if (!isset($prm['run_id']) || intval($prm['run_id']) <= 0) { $this->sys_error("Run ID is required"); exit; } $run_id = intval($prm['run_id']); $auto_track = isset($prm['auto_track_high_scores']) ? $prm['auto_track_high_scores'] : true; // Get run details $run = $this->ScreenerModel->get_screener_run($run_id); if (!$run) { $this->sys_error("Screener run not found"); exit; } // Get template to know scoring strategy $template = $this->ScreenerModel->get_template($run['screener_templates_template_id']); // Use template's min_score as default threshold $min_score_to_track = isset($prm['min_score_to_track']) ? floatval($prm['min_score_to_track']) : floatval($template['min_score'] ?? 8.0); // Get all unscored results for this run $results = $this->ScreenerModel->get_unscored_results($run_id); if (empty($results)) { $this->sys_error("No unscored results found for this run"); exit; } $scored_count = 0; $high_scores = array(); foreach ($results as $result) { // Score based on template configuration $score_data = $this->calculate_score($result, $template); // Add result_id and run_id $score_data['screener_results_result_id'] = $result['result_id']; $score_data['screener_runs_run_id'] = $run_id; $score_data['symbol'] = $result['symbol']; // Save score if ($this->ScreenerModel->save_score($score_data)) { $scored_count++; // Mark result as scored $this->ScreenerModel->mark_as_scored($result['result_id']); // Auto-track high scores if ($auto_track && $score_data['total_score'] >= $min_score_to_track) { $this->start_tracking($result['result_id']); $high_scores[] = array( 'symbol' => $result['symbol'], 'score' => $score_data['total_score'], 'decision' => $score_data['decision'] ); } } } $response = array( 'run_id' => $run_id, 'total_scored' => $scored_count, 'high_scores_tracked' => count($high_scores), 'high_scores' => $high_scores, 'next_step' => 'Monitor tracked positions via get_active_positions' ); $this->sys_ok($response); } catch (Exception $exc) { $this->sys_error($exc->getMessage()); } } /** * Update prices for all tracked positions * * POST /screener/screenertracker/update_prices * Body: { * "date": "2025-01-15", // Optional, defaults to today * "check_targets": true // Check if targets hit * } */ public function update_prices() { try { // Read JSON body manually for POST requests $json = file_get_contents('php://input'); $prm = json_decode($json, true); // Fallback to regular POST if JSON is empty if (empty($prm)) { $prm = $this->input->post(); } // Also check GET params as fallback if (empty($prm)) { $prm = $this->input->get(); } $tracking_date = isset($prm['date']) ? $prm['date'] : date('Y-m-d'); $check_targets = isset($prm['check_targets']) ? $prm['check_targets'] : true; // Get all active tracked positions $active_positions = $this->ScreenerModel->get_active_positions(); if (empty($active_positions)) { $this->sys_ok(array('message' => 'No active positions to update')); exit; } $updated_count = 0; $alerts = array(); foreach ($active_positions as $position) { // Fetch current price from Stockbit $price_data = $this->get_stock_price($position['symbol']); if (!$price_data) { continue; } // Calculate returns $entry_price = floatval($position['entry_price']); $current_price = floatval($price_data['lastprice']); $return_amount = $current_price - $entry_price; $return_percentage = ($return_amount / $entry_price) * 100; // Days since entry $entry_date = new DateTime($position['entry_date']); $today = new DateTime($tracking_date); $days_since_entry = $today->diff($entry_date)->days; $weeks_since_entry = floor($days_since_entry / 7); // Save price tracking $tracking_data = array( 'screener_results_result_id' => $position['screener_results_result_id'], 'symbol' => $position['symbol'], 'tracking_date' => $tracking_date, 'tracking_datetime' => date('Y-m-d H:i:s'), 'current_price' => $current_price, 'open_price' => $price_data['open'], 'high_price' => $price_data['high'], 'low_price' => $price_data['low'], 'volume' => $price_data['volume'], 'value' => $price_data['value'], 'frequency' => $price_data['frequency'], 'foreign_buy' => $price_data['fbuy'], 'foreign_sell' => $price_data['fsell'], 'foreign_net' => $price_data['fnet'], 'entry_price' => $entry_price, 'return_amount' => $return_amount, 'return_percentage' => $return_percentage, 'days_since_entry' => $days_since_entry, 'weeks_since_entry' => $weeks_since_entry ); if ($this->ScreenerModel->save_price_tracking($tracking_data)) { $updated_count++; // Update performance $this->update_performance($position['screener_results_result_id'], $return_percentage, $days_since_entry); // Check targets if enabled if ($check_targets) { $target_alerts = $this->check_targets($position, $return_percentage, $tracking_date); if (!empty($target_alerts)) { $alerts = array_merge($alerts, $target_alerts); } } } } $response = array( 'tracking_date' => $tracking_date, 'positions_tracked' => count($active_positions), 'prices_updated' => $updated_count, 'alerts_generated' => count($alerts), 'alerts' => $alerts ); $this->sys_ok($response); } catch (Exception $exc) { $this->sys_error($exc->getMessage()); } } /** * Get active positions * * GET /screener/screenertracker/get_active_positions * Query params: * - template_id (optional): Filter by template * - min_score (optional): Filter by minimum score * - sort_by: return_percentage, days_held, score (default: return_percentage) * - order: asc, desc (default: desc) */ public function get_active_positions() { try { // Read JSON body manually for POST requests $json = file_get_contents('php://input'); $prm = json_decode($json, true); // Fallback to regular POST if JSON is empty if (empty($prm)) { $prm = $this->input->post(); } // Also check GET params as fallback if (empty($prm)) { $prm = $this->input->get(); } $filters = array( 'template_id' => isset($prm['template_id']) ? intval($prm['template_id']) : null, 'min_score' => isset($prm['min_score']) ? floatval($prm['min_score']) : null, 'sort_by' => isset($prm['sort_by']) ? $prm['sort_by'] : 'return_percentage', 'order' => isset($prm['order']) ? $prm['order'] : 'desc' ); $positions = $this->ScreenerModel->get_active_positions_with_details($filters); $response = array( 'total_positions' => count($positions), 'positions' => $positions, 'summary' => $this->calculate_portfolio_summary($positions) ); $this->sys_ok($response); } catch (Exception $exc) { $this->sys_error($exc->getMessage()); } } /** * Get performance statistics * * GET /screener/screenertracker/get_statistics * Query params: * - template_id (required or 'all') * - period: ALL_TIME, LAST_3M, LAST_6M, LAST_1Y * - refresh: true to recalculate */ public function get_statistics() { try { // Read JSON body manually for POST requests $json = file_get_contents('php://input'); $prm = json_decode($json, true); // Fallback to regular POST if JSON is empty if (empty($prm)) { $prm = $this->input->post(); } // Also check GET params as fallback if (empty($prm)) { $prm = $this->input->get(); } $template_id = isset($prm['template_id']) ? $prm['template_id'] : 'all'; $period = isset($prm['period']) ? $prm['period'] : 'ALL_TIME'; $refresh = isset($prm['refresh']) && $prm['refresh'] === true; if ($refresh && $template_id !== 'all') { // Recalculate statistics $this->ScreenerModel->calculate_statistics(intval($template_id)); } if ($template_id === 'all') { $statistics = $this->ScreenerModel->get_all_statistics($period); } else { $statistics = $this->ScreenerModel->get_statistics(intval($template_id), $period); } $this->sys_ok(array('statistics' => $statistics)); } catch (Exception $exc) { $this->sys_error($exc->getMessage()); } } /** * Get all available templates * * GET /screener/screenertracker/get_templates * Response: List of all active screener templates with configuration */ public function get_templates() { try { $templates = $this->ScreenerModel->get_all_templates(); // Parse JSON configs for each template foreach ($templates as &$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); } } $this->sys_ok(array( 'total' => count($templates), 'templates' => $templates )); } catch (Exception $exc) { $this->sys_error($exc->getMessage()); } } /** * Get single template details * * GET /screener/screenertracker/get_template * Params: template_id or template_slug */ public function get_template() { try { // Read JSON body manually for POST requests $json = file_get_contents('php://input'); $prm = json_decode($json, true); // Fallback to regular POST if JSON is empty if (empty($prm)) { $prm = $this->input->post(); } // Also check GET params as fallback if (empty($prm)) { $prm = $this->input->get(); } $template_id = isset($prm['template_id']) ? $prm['template_id'] : null; $template_slug = isset($prm['template_slug']) ? $prm['template_slug'] : null; $template = null; if ($template_id) { $template = $this->ScreenerModel->get_template(intval($template_id)); } elseif ($template_slug) { $template = $this->ScreenerModel->get_template_by_slug($template_slug); } else { $this->sys_error("template_id or template_slug is required"); exit; } if (!$template) { $this->sys_error("Template not found"); exit; } $this->sys_ok($template); } catch (Exception $exc) { $this->sys_error($exc->getMessage()); } } /** * Manual Track Result - Start tracking a specific result * * POST /screener/tracker/track_result * Body: { * "result_id": 1 * } */ public function track_result() { try { // Read JSON body manually for POST requests $json = file_get_contents('php://input'); $prm = json_decode($json, true); // Fallback to regular POST if JSON is empty if (empty($prm)) { $prm = $this->input->post(); } // Also check GET params as fallback if (empty($prm)) { $prm = $this->input->get(); } if (!isset($prm['result_id'])) { $this->sys_error("Result ID is required"); exit; } $result_id = intval($prm['result_id']); // Get result to verify it exists and is scored $result = $this->ScreenerModel->get_result($result_id); if (!$result) { $this->sys_error("Result not found"); exit; } // Check if already tracked if ($result['is_tracked'] == 'Y') { $this->sys_error("Result is already being tracked"); exit; } // Check if scored if ($result['is_scored'] != 'Y') { $this->sys_error("Result must be scored before tracking. Run score_results first."); exit; } // Start tracking $this->start_tracking($result_id); $response = array( 'result_id' => $result_id, 'symbol' => $result['symbol'], 'entry_price' => $result['entry_price'], 'message' => 'Started tracking position', 'next_step' => 'Monitor via get_active_positions or update_prices' ); $this->sys_ok($response); } catch (Exception $exc) { $this->sys_error($exc->getMessage()); } } /** * Close position manually * * POST /screener/screenertracker/close_position * Body: { * "result_id": 1, * "exit_price": 2500, * "exit_reason": "TARGET_2", * "notes": "Manual exit" * } */ public function close_position() { try { // Read JSON body manually for POST requests $json = file_get_contents('php://input'); $prm = json_decode($json, true); // Fallback to regular POST if JSON is empty if (empty($prm)) { $prm = $this->input->post(); } // Also check GET params as fallback if (empty($prm)) { $prm = $this->input->get(); } if (!isset($prm['result_id'])) { $this->sys_error("Result ID is required"); exit; } $result_id = intval($prm['result_id']); $exit_price = isset($prm['exit_price']) ? floatval($prm['exit_price']) : null; $exit_reason = isset($prm['exit_reason']) ? $prm['exit_reason'] : 'MANUAL'; $notes = isset($prm['notes']) ? $prm['notes'] : ''; // Get performance record $performance = $this->ScreenerModel->get_performance_by_result($result_id); if (!$performance) { $this->sys_error("Performance record not found"); exit; } if ($performance['is_active'] === 'N') { $this->sys_error("Position already closed"); exit; } // If no exit price provided, get current price if (!$exit_price) { $price_data = $this->get_stock_price($performance['symbol']); if($price_data['status'] != 'OK') { $this->sys_error("Failed to get stock price"); exit; } $exit_price = $price_data['data']['lastprice']; } // Calculate final returns $entry_price = floatval($performance['entry_price']); $return_amount = $exit_price - $entry_price; $return_percentage = ($return_amount / $entry_price) * 100; $entry_date = new DateTime($performance['entry_date']); $exit_date = new DateTime(date('Y-m-d')); $days_held = $exit_date->diff($entry_date)->days; // Update performance $close_data = array( 'exit_date' => date('Y-m-d'), 'exit_price' => $exit_price, 'exit_reason' => $exit_reason, 'days_held' => $days_held, 'weeks_held' => floor($days_held / 7), 'return_amount' => $return_amount, 'return_percentage' => $return_percentage, 'is_active' => 'N', 'notes' => $notes, 'closed_at' => date('Y-m-d H:i:s') ); if ($this->ScreenerModel->close_performance($performance['performance_id'], $close_data)) { // Recalculate statistics $this->ScreenerModel->calculate_statistics($performance['screener_templates_template_id']); $response = array( 'message' => 'Position closed successfully', 'symbol' => $performance['symbol'], 'entry_price' => $entry_price, 'exit_price' => $exit_price, 'return_percentage' => round($return_percentage, 2), 'days_held' => $days_held, 'outcome' => $close_data['outcome'] ); $this->sys_ok($response); } else { $this->sys_error("Failed to close position"); } } catch (Exception $exc) { $this->sys_error($exc->getMessage()); } } /** * Get detailed performance for a symbol * * GET /screener/screenertracker/get_symbol_performance * Query params: * - symbol (required) * - include_price_history: true/false */ public function get_symbol_performance() { try { // Read JSON body manually for POST requests $json = file_get_contents('php://input'); $prm = json_decode($json, true); // Fallback to regular POST if JSON is empty if (empty($prm)) { $prm = $this->input->post(); } // Also check GET params as fallback if (empty($prm)) { $prm = $this->input->get(); } if (!isset($prm['symbol'])) { $this->sys_error("Symbol is required"); exit; } $symbol = strtoupper($prm['symbol']); $include_history = isset($prm['include_price_history']) && $prm['include_price_history']; // Get all performance records for this symbol $performances = $this->ScreenerModel->get_symbol_performances($symbol); if (empty($performances)) { $this->sys_error("No performance records found for symbol: " . $symbol); exit; } $response = array( 'symbol' => $symbol, 'total_trades' => count($performances), 'performances' => $performances ); if ($include_history) { // Get price history for latest active position $active = array_filter($performances, function($p) { return $p['is_active'] === 'Y'; }); if (!empty($active)) { $latest = reset($active); $price_history = $this->ScreenerModel->get_price_history($latest['screener_results_result_id']); $response['price_history'] = $price_history; } } $this->sys_ok($response); } catch (Exception $exc) { $this->sys_error($exc->getMessage()); } } // ============================================ // PRIVATE HELPER METHODS // ============================================ /** * Call Stockbit screener API */ private function call_stockbit_screener($template) { $url = $this->stockbit_base_url . '/screener/templates'; $post_data = array( 'name' => $template['template_name'], 'description' => $template['description'], 'save' => '0', 'ordertype' => 'asc', 'ordercol' => 2, 'page' => 1, 'universe' => '{"scope":"IHSG","scopeID":"","name":""}', 'filters' => $template['filters_json'], 'sequence' => $template['sequence'], 'screenerid' => '0', 'type' => 'TEMPLATE_TYPE_CUSTOM' ); $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($post_data)); curl_setopt($ch, CURLOPT_HTTPHEADER, array( 'Content-Type: application/json', 'Authorization: ' . $this->stockbit_token, 'Accept: application/json' )); $response = curl_exec($ch); $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($http_code !== 200) { return false; } return json_decode($response, true); } /** * Get stock price from Stockbit */ private function get_stock_price($symbol) { $url = $this->stockbit_base_url . '/company-price-feed/v2/orderbook/companies/' . $symbol; $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_HTTPHEADER, array( 'Authorization: ' . $this->stockbit_token, 'Accept: application/json' )); $response = curl_exec($ch); $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($http_code !== 200) { return false; } return json_decode($response, true); } /** * Calculate score based on strategy type */ private function calculate_score($result, $template) { $metrics = json_decode($result['metrics_json'], true); // Convert metrics array to associative array for easier lookup $metrics_map = array(); foreach ($metrics as $metric) { // Create keys from metric item names $key = $this->normalize_metric_key($metric['item']); $metrics_map[$key] = floatval($metric['raw']); // Also store with generic keys for common metrics if (strpos($metric['item'], 'Foreign') !== false) { if (strpos($metric['item'], 'Streak') !== false) { $metrics_map['foreign_streak'] = floatval($metric['raw']); } elseif (strpos($metric['item'], '1M') !== false || strpos($metric['item'], '1 Month') !== false) { $metrics_map['foreign_flow_1m'] = floatval($metric['raw']); } } if (strpos($metric['item'], 'Bandar') !== false) { if (strpos($metric['item'], 'Accum') !== false) { $metrics_map['bandar_accum_dist'] = floatval($metric['raw']); } } if (strpos($metric['item'], 'Price Return') !== false) { if (strpos($metric['item'], '1 Day') !== false) { $metrics_map['1d_return'] = floatval($metric['raw']); } elseif (strpos($metric['item'], '1 Week') !== false) { $metrics_map['1w_return'] = floatval($metric['raw']); } elseif (strpos($metric['item'], '1 Month') !== false) { $metrics_map['1m_return'] = floatval($metric['raw']); } } if (strpos($metric['item'], 'Volume Change') !== false) { $metrics_map['1d_vol_change'] = floatval($metric['raw']); } } // Use flexible scoring from model $score_result = $this->ScreenerModel->calculate_score_from_template($template, $metrics_map); // Format for database storage $score_data = array( 'category_1_score' => 0, 'category_1_name' => 'Category 1', 'category_1_max' => 4, 'category_2_score' => 0, 'category_2_name' => 'Category 2', 'category_2_max' => 4, 'category_3_score' => 0, 'category_3_name' => 'Category 3', 'category_3_max' => 2, 'category_4_score' => 0, 'category_4_name' => 'Category 4', 'category_4_max' => 2, 'total_score' => $score_result['total_score'], 'max_possible_score' => $score_result['max_possible_score'], 'decision' => $score_result['decision'], 'confidence' => $score_result['confidence'], 'suggested_position_size' => $score_result['suggested_position_size'], 'scoring_notes' => isset($score_result['scoring_notes']) ? json_encode($score_result['scoring_notes']) : null ); // Fill in category scores if (!empty($score_result['category_scores'])) { foreach ($score_result['category_scores'] as $cat_key => $cat_data) { $score_data[$cat_key . '_score'] = $cat_data['score']; $score_data[$cat_key . '_name'] = $cat_data['name']; $score_data[$cat_key . '_max'] = $cat_data['max']; } } return $score_data; } /** * Normalize metric key for easy matching */ private function normalize_metric_key($item_name) { // Convert to lowercase and remove special characters $key = strtolower($item_name); $key = str_replace(array(' ', '/', '-'), '_', $key); $key = preg_replace('/[^a-z0-9_]/', '', $key); return $key; } // OLD SCORING METHODS REMOVED - Now using flexible scoring from ScreenerModel // All scoring logic is now in ScreenerModel->calculate_score_from_template() // which reads from scoring_config_json in database /** * Start tracking a result */ private function start_tracking($result_id) { $this->ScreenerModel->mark_as_tracked($result_id); // Create initial performance record $result = $this->ScreenerModel->get_result($result_id); $score = $this->ScreenerModel->get_score_by_result($result_id); $performance_data = array( 'screener_results_result_id' => $result_id, 'screener_scores_score_id' => $score['score_id'], 'symbol' => $result['symbol'], 'screener_templates_template_id' => $result['screener_templates_template_id'], 'entry_date' => $result['entry_date'], 'entry_price' => $result['entry_price'], 'entry_score' => $score['total_score'], 'entry_decision' => $score['decision'], 'is_active' => 'Y', 'win_loss' => 'OPEN' ); $this->ScreenerModel->create_performance($performance_data); // Create initial price tracking $price_data = $this->get_stock_price($result['symbol']); if ($price_data) { $tracking_data = array( 'screener_results_result_id' => $result_id, 'symbol' => $result['symbol'], 'tracking_date' => date('Y-m-d'), 'tracking_datetime' => date('Y-m-d H:i:s'), 'current_price' => $price_data['lastprice'], 'entry_price' => $result['entry_price'], 'return_amount' => 0, 'return_percentage' => 0, 'days_since_entry' => 0, 'weeks_since_entry' => 0 ); $this->ScreenerModel->save_price_tracking($tracking_data); } } /** * Update performance record */ private function update_performance($result_id, $return_pct, $days_held) { $performance = $this->ScreenerModel->get_performance_by_result($result_id); if (!$performance) { return; } // Update max gain/loss $update_data = array( 'return_percentage' => $return_pct, 'days_held' => $days_held, 'weeks_held' => floor($days_held / 7) ); if ($return_pct > floatval($performance['max_gain_percentage'])) { $update_data['max_gain_percentage'] = $return_pct; $update_data['max_gain_date'] = date('Y-m-d'); } if ($return_pct < floatval($performance['max_loss_percentage'])) { $update_data['max_loss_percentage'] = $return_pct; $update_data['max_loss_date'] = date('Y-m-d'); } $this->ScreenerModel->update_performance($performance['performance_id'], $update_data); } /** * Check if targets hit and generate alerts */ private function check_targets($position, $return_pct, $date) { $alerts = array(); $performance = $this->ScreenerModel->get_performance_by_result($position['screener_results_result_id']); // Target 1: +15-20% if ($return_pct >= 15 && $performance['target_1_hit'] === 'N') { $this->ScreenerModel->mark_target_hit($performance['performance_id'], 1, $date); $alerts[] = array( 'symbol' => $position['symbol'], 'type' => 'TARGET_1', 'return' => round($return_pct, 2), 'message' => 'Target 1 (+15%) hit! Consider taking 30% profit' ); } // Target 2: +30-40% if ($return_pct >= 30 && $performance['target_2_hit'] === 'N') { $this->ScreenerModel->mark_target_hit($performance['performance_id'], 2, $date); $alerts[] = array( 'symbol' => $position['symbol'], 'type' => 'TARGET_2', 'return' => round($return_pct, 2), 'message' => 'Target 2 (+30%) hit! Consider taking 50% profit' ); } // Target 3: +50%+ if ($return_pct >= 50 && $performance['target_3_hit'] === 'N') { $this->ScreenerModel->mark_target_hit($performance['performance_id'], 3, $date); $alerts[] = array( 'symbol' => $position['symbol'], 'type' => 'TARGET_3', 'return' => round($return_pct, 2), 'message' => 'Target 3 (+50%) hit! Consider exiting remaining position' ); } // Stop loss: -8% to -10% if ($return_pct <= -8) { $alerts[] = array( 'symbol' => $position['symbol'], 'type' => 'STOP_LOSS', 'return' => round($return_pct, 2), 'message' => 'Stop loss triggered! Consider exiting position', 'level' => 'CRITICAL' ); } // Time stop: 8 weeks if ($position['weeks_held'] >= 8 && $return_pct < 10) { $alerts[] = array( 'symbol' => $position['symbol'], 'type' => 'TIME_STOP', 'return' => round($return_pct, 2), 'weeks' => $position['weeks_held'], 'message' => '8 weeks held with <10% return. Consider exiting', 'level' => 'WARNING' ); } // Save alerts to database foreach ($alerts as $alert) { $this->ScreenerModel->save_alert($position['screener_results_result_id'], $alert); } return $alerts; } /** * Calculate portfolio summary */ private function calculate_portfolio_summary($positions) { if (empty($positions)) { return array(); } $total_positions = count($positions); $total_return = 0; $total_invested = 0; $winners = 0; $losers = 0; foreach ($positions as $pos) { $return_pct = floatval($pos['return_percentage']); $total_return += $return_pct; if ($return_pct > 0) { $winners++; } elseif ($return_pct < 0) { $losers++; } } $avg_return = $total_return / $total_positions; return array( 'total_positions' => $total_positions, 'winners' => $winners, 'losers' => $losers, 'breakeven' => $total_positions - $winners - $losers, 'avg_return_percentage' => round($avg_return, 2), 'best_performer' => $positions[0] ?? null, 'worst_performer' => $positions[count($positions) - 1] ?? null ); } }