Files
2026-04-27 10:26:26 +07:00

1201 lines
43 KiB
PHP

<?php
class Tracker extends CI_Controller
{
var $stockbit_token;
var $stockbit_base_url;
public function index()
{
echo "Resultentry API";
}
public function __construct()
{
parent::__construct();
parent::__construct();
$this->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
);
}
}