← 返回 下载 api.php
<?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);
}