$url, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => $timeout, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_HTTPHEADER => ['Referer: https://finance.sina.com.cn'], CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' ]); $r = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode == 200 && !empty($r)) return $r; if ($i < $retry - 1) usleep(300000 * ($i + 1)); } return ''; } function getMarketIndices() { $cache = jread('market_cache'); if (isset($cache['t']) && time() - $cache['t'] < 10) { return $cache['data'] ?? []; } $resp = curlGet("https://hq.sinajs.cn/list=s_sh000001,s_sz399001,s_sz399006", 5, 1); $indices = []; $names = ['上证', '深证', '创业板']; if (!empty($resp)) { $resp = mb_convert_encoding($resp, 'UTF-8', 'GBK'); $lines = explode("\n", trim($resp)); foreach ($lines as $i => $line) { if (preg_match('/"([^"]+)"/', $line, $m)) { $d = explode(',', $m[1]); if (count($d) > 3) { $price = floatval($d[3]); $yest = floatval($d[2]); $pct = $yest > 0 ? round(($price - $yest) / $yest * 100, 2) : 0; $indices[] = [ 'name' => $names[$i] ?? '指数', 'price' => $price, 'pct' => $pct, 'volume' => floatval($d[8] ?? 0), 'amount' => floatval($d[9] ?? 0) ]; } } } } $data = $indices; jsave('market_cache', ['data' => $data, 't' => time()]); return $data; } function batchGetStocks($codes) { $results = []; foreach (array_chunk($codes, 80) as $batch) { $fullCodes = array_map(function($c) { return (strpos($c, '6') === 0 ? 'sh' : 'sz') . $c; }, $batch); $resp = curlGet("https://hq.sinajs.cn/list=" . implode(',', $fullCodes)); if (empty($resp)) continue; $resp = mb_convert_encoding($resp, 'UTF-8', 'GBK'); foreach (explode("\n", trim($resp)) as $line) { preg_match('/var hq_str_(?:sh|sz)(\d+)="([^"]+)"/', $line, $m); if (empty($m)) continue; $d = explode(',', $m[2]); if (count($d) < 32) continue; $price = floatval($d[3]); $yest = floatval($d[2]); $code = $m[1]; $isChuang = (strpos($code, '300') === 0 || strpos($code, '688') === 0 || strpos($code, '301') === 0); if ($isChuang) { $rawTurnover = floatval($d[37] ?? 0); $pe = floatval($d[44] ?? 0); $floatCap = floatval($d[45] ?? 0); if ($floatCap > 10000000) $floatCap = round($floatCap / 100000000, 2); } else { $rawTurnover = floatval($d[10] ?? 0); $pe = floatval($d[38] ?? 0); $floatCap = floatval($d[45] ?? 0) / 100000000; } if ($rawTurnover > 50) $rawTurnover = round($rawTurnover / 100, 2); $results[$code] = [ 'name' => $d[0], 'code' => $code, 'price' => $price, 'pct' => $yest > 0 ? round(($price - $yest) / $yest * 100, 2) : 0, 'open' => floatval($d[1]), 'high' => floatval($d[4]), 'low' => floatval($d[5]), 'yest' => $yest, 'volume' => floatval($d[8]), 'amount' => floatval($d[9]), 'turnover' => $rawTurnover, 'pe' => $pe, 'float_cap' => $floatCap, 'pb' => floatval($d[46] ?? 0) / 100 // PB来自新浪数据位46 ]; } } return $results; } function getKlineData($code, $days = 500) { $cache = jread('kline_cache'); $now = new DateTime(); $today = $now->format('Y-m-d'); $hour = (int)$now->format('H'); $dayOfWeek = (int)$now->format('N'); $isTradeTime = ($dayOfWeek <= 5 && $hour >= 9 && $hour < 15); if (!$isTradeTime && isset($cache[$code]) && substr($cache[$code]['t'] ?? '', 0, 10) === $today && count($cache[$code]['d'] ?? []) > 300) return $cache[$code]['d']; if ($isTradeTime && isset($cache[$code]) && time() - strtotime($cache[$code]['t']) < 300) return $cache[$code]['d']; $fullCode = (strpos($code, '6') === 0 ? 'sh' : 'sz') . $code; $resp = curlGet("https://money.finance.sina.com.cn/quotes_service/api/json_v2.php/CN_MarketData.getKLineData?symbol={$fullCode}&scale=240&ma=no&datalen={$days}"); if (empty($resp)) { if (isset($cache[$code]) && count($cache[$code]['d'] ?? []) > 60) return $cache[$code]['d']; return []; } $json = json_decode($resp, true); if (!$json) { if (isset($cache[$code]) && count($cache[$code]['d'] ?? []) > 60) return $cache[$code]['d']; return []; } $kl = []; foreach ($json as $item) $kl[] = ['date' => $item['day'] ?? '', 'open' => floatval($item['open'] ?? 0), 'close' => floatval($item['close'] ?? 0), 'high' => floatval($item['high'] ?? 0), 'low' => floatval($item['low'] ?? 0), 'volume' => floatval($item['volume'] ?? 0)]; $cache[$code] = ['d' => $kl, 't' => $now->format('Y-m-d H:i:s')]; jsave('kline_cache', $cache); return $kl; } function StdDev($arr) { $n = count($arr); if ($n < 2) return 0; $mean = array_sum($arr) / $n; $sum = 0; foreach ($arr as $v) $sum += ($v - $mean) ** 2; return sqrt($sum / ($n - 1)); } function scoreStock($code, $kl, $stockInfo = null) { $n = count($kl); if ($n < 60) return null; $cl = array_column($kl, 'close'); $vl = array_column($kl, 'volume'); $current = $cl[$n-1]; // ========== 动量因子 ========== $mom5 = $n >= 6 && $cl[$n-6] > 0 ? ($current - $cl[$n-6]) / $cl[$n-6] : 0; $mom10 = $n >= 11 && $cl[$n-11] > 0 ? ($current - $cl[$n-11]) / $cl[$n-11] : 0; $mom20 = $n >= 21 && $cl[$n-21] > 0 ? ($current - $cl[$n-21]) / $cl[$n-21] : 0; $mom60 = $n >= 61 && $cl[$n-61] > 0 ? ($current - $cl[$n-61]) / $cl[$n-61] : 0; $mom120 = $n >= 121 && $cl[$n-121] > 0 ? ($current - $cl[$n-121]) / $cl[$n-121] : 0; // ========== 波动率因子 ========== $rets = []; for ($i = max(1, $n-20); $i < $n; $i++) { if ($cl[$i-1] > 0) $rets[] = ($cl[$i] - $cl[$i-1]) / $cl[$i-1]; } $volatility = count($rets) > 1 ? StdDev($rets) : 0.03; $downRets = array_filter($rets, function($r) { return $r < 0; }); $downsideVol = count($downRets) > 1 ? StdDev(array_values($downRets)) : $volatility; $priceChanges = []; for ($i = max(1, $n-60); $i < $n; $i++) { if ($cl[$i-1] > 0) $priceChanges[] = ($cl[$i] - $cl[$i-1]) / $cl[$i-1]; } $beta = count($priceChanges) > 1 ? StdDev($priceChanges) / ($volatility + 0.001) : 1; $beta = max(0.3, min(3, $beta)); // ========== 均线因子 ========== $ma20 = array_sum(array_slice($cl, -20)) / 20; $ma60 = array_sum(array_slice($cl, -60)) / 60; $ma120 = $n >= 120 ? array_sum(array_slice($cl, -120)) / 120 : $ma60; $distMa20 = $ma20 > 0 ? ($current - $ma20) / $ma20 : 0; $distMa60 = $ma60 > 0 ? ($current - $ma60) / $ma60 : 0; $maAlignment = ($ma20 > $ma60 && $ma60 > $ma120) ? 1 : (($ma20 > $ma60) ? 0.5 : 0); // ========== 成交量因子 ========== $v5 = array_sum(array_slice($vl, -5)) / 5; $v20 = array_sum(array_slice($vl, -20)) / 20; $volRatio = $v20 > 0 ? $v5 / $v20 : 1; $priceDown = $cl[$n-1] < $cl[$n-6]; $volShrink = $volRatio < 0.7; $shrinkScore = ($priceDown && $volShrink) ? 3 : 0; // ========== RSI因子 ========== $g = 0; $l = 0; for ($i = max(1, $n-14); $i < $n; $i++) { $d = $cl[$i] - $cl[$i-1]; if ($d > 0) $g += $d; else $l += abs($d); } $rsi = $l > 0 ? 100 - 100 / (1 + $g / $l) : 50; $amount = array_sum(array_slice($vl, -5)) / 5; // ========== 🆕 质量因子 ========== $qualityScore = 0; // 最大回撤 $maxDrawdown = 0; $peak = $cl[$n-60]; for ($i = $n-60; $i < $n; $i++) { if ($cl[$i] > $peak) $peak = $cl[$i]; $drawdown = $peak > 0 ? ($peak - $cl[$i]) / $peak : 0; if ($drawdown > $maxDrawdown) $maxDrawdown = $drawdown; } if ($maxDrawdown < 0.10) $qualityScore += 10; elseif ($maxDrawdown < 0.15) $qualityScore += 8; elseif ($maxDrawdown < 0.25) $qualityScore += 4; elseif ($maxDrawdown > 0.40) $qualityScore -= 6; elseif ($maxDrawdown > 0.30) $qualityScore -= 3; // 收益稳定性 $monthlyRets = []; for ($i = 20; $i < $n; $i += 20) { if ($cl[$i-20] > 0) $monthlyRets[] = ($cl[$i] - $cl[$i-20]) / $cl[$i-20]; } $positiveMonths = count(array_filter($monthlyRets, function($r) { return $r > 0; })); $stabilityRatio = count($monthlyRets) > 0 ? $positiveMonths / count($monthlyRets) : 0.5; if ($stabilityRatio > 0.75) $qualityScore += 6; elseif ($stabilityRatio > 0.60) $qualityScore += 3; elseif ($stabilityRatio < 0.40) $qualityScore -= 4; // ========== 🆕 价值因子 ========== $valueScore = 0; if ($stockInfo) { $pe = $stockInfo['pe'] ?? 0; $pb = $stockInfo['pb'] ?? 0; $floatCap = $stockInfo['float_cap'] ?? 0; // PE因子 if ($pe > 0 && $pe < 10) $valueScore += 12; elseif ($pe > 0 && $pe < 15) $valueScore += 8; elseif ($pe > 0 && $pe < 20) $valueScore += 5; elseif ($pe > 0 && $pe < 30) $valueScore += 2; elseif ($pe > 60) $valueScore -= 5; elseif ($pe > 100) $valueScore -= 8; // PB因子 if ($pb > 0 && $pb < 1) $valueScore += 10; elseif ($pb > 0 && $pb < 1.5) $valueScore += 6; elseif ($pb > 0 && $pb < 3) $valueScore += 2; elseif ($pb > 8) $valueScore -= 5; // 市值因子(小盘溢价) if ($floatCap > 0 && $floatCap < 100) $valueScore += 4; elseif ($floatCap > 0 && $floatCap < 300) $valueScore += 2; elseif ($floatCap > 5000) $valueScore -= 2; } else { // 无基本面数据,用价格位置代理 if ($distMa60 < -0.20) $valueScore += 6; elseif ($distMa60 < -0.10) $valueScore += 3; elseif ($distMa60 > 0.40) $valueScore -= 4; } // ========== 🆕 分红因子 ========== $dividendScore = 0; if ($stockInfo) { $floatCap = $stockInfo['float_cap'] ?? 0; $pe = $stockInfo['pe'] ?? 0; // 大市值 + 低PE + 低波动 ≈ 红利股 if ($floatCap > 1000 && $pe > 0 && $pe < 20 && $volatility < 0.025) { $dividendScore += 8; } elseif ($floatCap > 500 && $pe > 0 && $pe < 30 && $volatility < 0.03) { $dividendScore += 4; } } // 低波动 + 稳定上涨 + 小回撤 = 红利股技术特征 if ($volatility < 0.02 && $mom60 > 0.05 && $maxDrawdown < 0.20) { $dividendScore += 5; } elseif ($volatility < 0.025 && $stabilityRatio > 0.6) { $dividendScore += 2; } // ========== 综合评分 ========== $score = 0; // 动量因子 $score += $mom5 * 25; $score += $mom10 * 15; $score += $mom20 * 8; $score += $mom60 * 5; $score += $mom120 * 3; // 波动率因子 $score += max(0, (0.03 - $volatility) * 40); $score += max(0, (0.025 - $downsideVol) * 20); if ($beta < 0.8) $score += 3; elseif ($beta > 1.5) $score -= 2; // 均线因子 $score += $distMa20 * 10; $score += $maAlignment * 5; // 成交量因子 if ($volRatio > 1.2 && $mom5 > 0) $score += 5; elseif ($volRatio > 1.2) $score += 2; if ($volRatio < 0.5) $score -= 4; $score += $shrinkScore; // RSI因子 if ($rsi > 40 && $rsi < 70) $score += 5; if ($rsi < 30) $score -= 8; if ($rsi > 80) $score -= 5; // 流动性 if ($amount > 100000) $score += 5; elseif ($amount < 10000) $score -= 3; // 🆕 三大新因子 $score += $qualityScore; $score += $valueScore; $score += $dividendScore; // 止损止盈 $stopLoss = round($current * (1 - $volatility * 2.5), 2); $takeProfit = round($current * (1 + $volatility * 5), 2); // 仓位计算 $histRets = []; for ($i = 60; $i < $n - 5; $i += 5) { if ($cl[$i] > 0) $histRets[] = ($cl[$i+5] - $cl[$i]) / $cl[$i]; } $wins = count(array_filter($histRets, function($r) { return $r > 0; })); $winRate = count($histRets) > 0 ? $wins / count($histRets) : 0.5; $avgWin = $wins > 0 ? array_sum(array_filter($histRets, function($r) { return $r > 0; })) / $wins : 0.02; $losses = count($histRets) - $wins; $avgLoss = $losses > 0 ? abs(array_sum(array_filter($histRets, function($r) { return $r < 0; }))) / $losses : 0.02; $b = $avgLoss > 0 ? $avgWin / $avgLoss : 1; $kelly = $b > 0 ? max(0, ($b * $winRate - (1 - $winRate)) / $b) : 0; $position = round($kelly * 0.5 * 100, 1); return [ 'code' => $code, 'score' => round($score, 1), 'mom5' => round($mom5 * 100, 1), 'mom10' => round($mom10 * 100, 1), 'mom20' => round($mom20 * 100, 1), 'mom60' => round($mom60 * 100, 1), 'volatility' => round($volatility * 100, 1), 'downside_vol' => round($downsideVol * 100, 1), 'beta' => round($beta, 2), 'rsi' => round($rsi, 1), 'max_drawdown' => round($maxDrawdown * 100, 1), 'dist_ma20' => round($distMa20 * 100, 1), 'dist_ma60' => round($distMa60 * 100, 1), 'ma_alignment' => $maAlignment, 'vol_ratio' => round($volRatio, 2), 'quality_score' => $qualityScore, 'value_score' => $valueScore, 'dividend_score' => $dividendScore, 'stability' => round($stabilityRatio * 100, 1), 'stop_loss' => $stopLoss, 'take_profit' => $takeProfit, 'position' => $position, 'win_rate' => round($winRate * 100, 1) ]; } function rankAndBacktest($codes, $lookback = 250) { $currentResults = []; $backtestResults = []; $stockInfos = batchGetStocks($codes); foreach ($codes as $code) { $kl = getKlineData($code, 500); if (count($kl) < $lookback) continue; $stockInfo = $stockInfos[$code] ?? null; $currentScore = scoreStock($code, $kl, $stockInfo); if ($currentScore) { $stock = $stockInfo; if ($stock) { $currentScore['name'] = $stock['name']; $currentScore['price'] = $stock['price']; $currentScore['pct'] = $stock['pct']; $currentScore['turnover'] = $stock['turnover']; $currentScore['pe'] = $stock['pe']; $currentScore['pb'] = $stock['pb'] ?? 0; $currentScore['float_cap'] = $stock['float_cap']; $currentResults[] = $currentScore; } } $cl = array_column($kl, 'close'); for ($i = $lookback; $i < count($kl) - 10; $i += 10) { $hd = array_slice($kl, 0, $i + 1); $score = scoreStock($code, $hd, null); if (!$score) continue; $startPrice = $cl[$i]; $endPrice = $cl[$i + 10]; if ($startPrice <= 0) continue; $actualRet = ($endPrice - $startPrice) / $startPrice; $stopPrice = $score['stop_loss']; $lowestPrice = min(array_slice(array_column($hd, 'low'), -10)); $stoppedOut = $lowestPrice <= $stopPrice; $backtestResults[] = [ 'code' => $code, 'date' => $kl[$i]['date'], 'score' => $score['score'], 'actual_return' => round($actualRet * 100, 2), 'stopped_out' => $stoppedOut, 'return_with_stop' => $stoppedOut ? round(($stopPrice - $startPrice) / $startPrice * 100, 2) : round($actualRet * 100, 2) ]; } } usort($currentResults, function($a, $b) { return $b['score'] <=> $a['score']; }); usort($backtestResults, function($a, $b) { return $b['score'] <=> $a['score']; }); $btStats = ['total' => count($backtestResults), 'avg_return' => 0, 'sharpe' => 0, 'top5_accuracy' => 0, 'top5_avg_return' => 0, 'stopped_count' => 0]; if (count($backtestResults) > 10) { $topQuintile = array_slice($backtestResults, 0, max(1, floor(count($backtestResults) / 5))); $bottomQuintile = array_slice($backtestResults, -max(1, floor(count($backtestResults) / 5))); $topRet = array_sum(array_column($topQuintile, 'actual_return')) / count($topQuintile); $bottomRet = array_sum(array_column($bottomQuintile, 'actual_return')) / count($bottomQuintile); $allRets = array_column($backtestResults, 'actual_return'); $btStats['avg_return'] = round(array_sum($allRets) / count($allRets), 2); $btStats['sharpe'] = count($allRets) > 1 ? round((array_sum($allRets) / count($allRets)) / (StdDev($allRets) + 0.001), 2) : 0; $btStats['top5_avg_return'] = round($topRet, 2); $btStats['spread'] = round($topRet - $bottomRet, 2); $btStats['top5_accuracy'] = round(count(array_filter($topQuintile, function($r) { return $r['actual_return'] > 0; })) / count($topQuintile) * 100, 1); $stopRets = array_column($backtestResults, 'return_with_stop'); $btStats['avg_return_with_stop'] = round(array_sum($stopRets) / count($stopRets), 2); $btStats['stopped_count'] = count(array_filter($backtestResults, function($r) { return $r['stopped_out']; })); } return ['current' => $currentResults, 'backtest' => $btStats]; } // ===== 路由 ===== $action = $_POST['action'] ?? $_GET['action'] ?? ''; if ($action == 'scan') { header('Content-Type: application/json; charset=utf-8'); set_time_limit(60); $codes = [ '600519','000858','300750','002594','601318','600036','000333','600900','601899', '600276','300059','002230','688981','601012','600030','000100','002475','600809', '600585','601166','601398','600050','000063','002415','000977','300474', '601857','600028','601088','600150','601111','000002','000725','002156', '002460','300014','600438','601865','000001','002142','600016', '600887','000895','603288','300760','688185','300347','601127','600733', '002920','601689','600741','002050','600893','000768','002025', '600879','000547','600031','000157','600010','000932','601668', '601390','601800','601919','600026','300433','601138','002456','002241', '002049','603501','600460','002368','300738','603881' ]; $results = rankAndBacktest($codes); $indices = getMarketIndices(); echo json_encode(['ok' => true, 'data' => $results, 'indices' => $indices], JSON_UNESCAPED_UNICODE); exit; } if ($action == 'indices') { header('Content-Type: application/json; charset=utf-8'); echo json_encode(['ok' => true, 'data' => getMarketIndices()], JSON_UNESCAPED_UNICODE); exit; } if ($action == 'analyze') { header('Content-Type: application/json; charset=utf-8'); $code = trim($_GET['code'] ?? ''); if (!preg_match('/^\d{6}$/', $code)) { echo json_encode(['ok' => false, 'error' => '请输入6位代码']); exit; } $pool = batchGetStocks([$code]); $s = $pool[$code] ?? null; if (!$s) { echo json_encode(['ok' => false, 'error' => '未找到']); exit; } $kl = getKlineData($code, 500); if (count($kl) < 60) { echo json_encode(['ok' => false, 'error' => '数据不足']); exit; } $score = scoreStock($code, $kl, $s); $score['name'] = $s['name']; $score['price'] = $s['price']; $score['pct'] = $s['pct']; $score['open_price'] = $s['open']; $score['turnover'] = $s['turnover']; $score['pe'] = $s['pe']; $score['pb'] = $s['pb'] ?? 0; $score['float_cap'] = $s['float_cap']; $n = count($kl); $cl = array_column($kl, 'close'); $btHits = 0; $btTotal = 0; $btRets = []; for ($i = 200; $i < $n - 10; $i += 10) { $hd = array_slice($kl, 0, $i + 1); $btScore = scoreStock($code, $hd, null); if (!$btScore || $btScore['score'] < 5) continue; $startPrice = $cl[$i]; $endPrice = $cl[$i + 10]; if ($startPrice <= 0) continue; $ac = ($endPrice - $startPrice) / $startPrice; $btRets[] = $ac; $btTotal++; if ($ac > 0) $btHits++; } $score['backtest_total'] = $btTotal; $score['backtest_accuracy'] = $btTotal > 5 ? round($btHits / $btTotal * 100, 1) : 0; $score['backtest_avg_return'] = $btTotal > 0 ? round(array_sum($btRets) / $btTotal * 100, 2) : 0; $saveData = ['time' => date('Y-m-d H:i:s'), 'code' => $code, 'name' => $s['name'], 'price' => $s['price'], 'open_price' => $s['open'], 'pct' => $s['pct'], 'score' => $score]; jsave('stocks/' . $code, $saveData); echo json_encode(['ok' => true, 'data' => $score], JSON_UNESCAPED_UNICODE); exit; } if ($action == 'check') { header('Content-Type: application/json; charset=utf-8'); $code = trim($_GET['code'] ?? ''); if (!preg_match('/^\d{6}$/', $code)) { echo json_encode(['ok' => false, 'error' => '请输入6位代码']); exit; } $saved = jread('stocks/' . $code); if (empty($saved)) { echo json_encode(['ok' => false, 'error' => '未找到记录']); exit; } $pool = batchGetStocks([$code]); $current = $pool[$code] ?? null; if (!$current) { echo json_encode(['ok' => false, 'error' => '无法获取实时价格']); exit; } $actualPrice = $current['price']; $openPrice = $saved['open_price'] ?? $saved['price']; $dailyChange = $openPrice > 0 ? round(($actualPrice - $openPrice) / $openPrice * 100, 2) : 0; $totalChange = round(($actualPrice - $saved['price']) / $saved['price'] * 100, 2); $saved['checked_at'] = date('Y-m-d H:i:s'); $saved['actual_price'] = $actualPrice; $saved['daily_change'] = $dailyChange; $saved['total_change'] = $totalChange; $saved['verified'] = true; jsave('stocks/' . $code, $saved); echo json_encode(['ok' => true, 'data' => $saved], JSON_UNESCAPED_UNICODE); exit; } ?> QuantX · 多因子量化系统
QuantX
6-FACTOR
📊
QuantX 六因子量化系统
动量 · 低波动 · 质量 · 价值 · 分红 · 成交量
六大因子综合评分,多维度筛选优质标的