<?php
/**
* PHP 版 HTTP API(可挂在现有 nginx + PHP-FPM 环境下)。
*
* GET /api.php?risk=稳健&horizon=波段&style=momentum=0.4,low_vol=0.3,trend=0.3&positions=12
*
* 返回 JSON:策略规格 + 最新截面选股信号。
* 部署:把 quantlab-php/ 目录放到站点下,确保 quantlab/data/kline/*.csv 缓存可读,
* 或由 FactorEngine::fetchKline 自行抓取并写缓存(需写权限)。
*/
require __DIR__ . '/src/StrategyGenerator.php';
require __DIR__ . '/src/FactorEngine.php';
header('Content-Type: application/json; charset=utf-8');
$q = $_GET;
$risk = isset($q['risk']) ? $q['risk'] : '平衡';
$horizon = isset($q['horizon']) ? $q['horizon'] : '波段';
$style = null;
if (!empty($q['style'])) {
$style = [];
foreach (explode(',', $q['style']) as $kv) {
$parts = explode('=', $kv);
if (count($parts) === 2) {
$style[trim($parts[0])] = (float)$parts[1];
}
}
}
try {
$cfg = StrategyGenerator::generate($risk, $horizon, $style, [
'max_positions' => isset($q['positions']) ? (int)$q['positions'] : null,
'rebalance_days' => isset($q['rebalance']) ? (int)$q['rebalance'] : null,
'trend_filter' => !isset($q['no_trend_filter']),
]);
$codeFile = realpath(__DIR__ . '/../quantlab/data/universe_sample.csv');
$dataDir = realpath(__DIR__ . '/../quantlab/data/kline');
$codes = [];
if ($codeFile && is_file($codeFile)) {
foreach (file($codeFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
$line = trim($line, "\xEF\xBB\xBF");
if ($line === '' || strpos($line, '#') === 0 || strpos($line, 'code,') === 0) {
continue;
}
$codes[] = explode(',', $line)[0];
}
}
$universe = [];
foreach ($codes as $code) {
$kline = FactorEngine::loadCsv($dataDir . '/' . $code . '.csv');
if (!$kline) {
$kline = FactorEngine::fetchKline($code);
}
if (count($kline) >= 30) {
$universe[$code] = $kline;
}
}
$asOf = isset($q['date']) ? $q['date'] : null;
if (!$asOf) {
$latest = '';
foreach ($universe as $kline) {
$keys = array_keys($kline);
$last = $keys[count($keys) - 1];
if ($last > $latest) {
$latest = $last;
}
}
$asOf = $latest;
}
$engine = new FactorEngine($cfg);
$result = $engine->selectAt($asOf, $universe);
echo json_encode([
'as_of' => $asOf,
'universe_size' => count($universe),
'eligible' => $result['eligible'],
'config' => $cfg,
'holdings' => array_map(function ($code, $w) {
return ['code' => (string)$code, 'weight' => round($w, 4)];
}, array_keys($result['holdings']), array_values($result['holdings'])),
'engine' => 'php',
'disclaimer' => '输出为量化信号,仅供研究参考,不构成投资建议。市场有风险,投资需谨慎。',
], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
} catch (Exception $e) {
http_response_code(400);
echo json_encode(['error' => $e->getMessage()], JSON_UNESCAPED_UNICODE);
}