<?php
/**
 * 策略生成器（PHP 版）：把偏好参数翻译成完整策略规格。
 * 与 quantlab/strategy_generator.py 保持同一套映射规则，配置可互换。
 *
 * PHP 7.3+ 兼容，无外部依赖。
 */

class StrategyGenerator
{
    /** @var array 风险偏好定义域 */
    public static $riskLevels = [
        '保守' => ['target_vol' => 0.06, 'max_dd_derisk' => 0.08, 'max_positions' => 20, 'max_weight' => 0.08, 'low_vol_bonus' => 0.20, 'cash_buffer' => 0.15, 'desc' => '低波动优先，深度分散，回撤容忍度低'],
        '稳健' => ['target_vol' => 0.10, 'max_dd_derisk' => 0.12, 'max_positions' => 15, 'max_weight' => 0.10, 'low_vol_bonus' => 0.12, 'cash_buffer' => 0.10, 'desc' => '波动可控，适度分散'],
        '平衡' => ['target_vol' => 0.15, 'max_dd_derisk' => 0.18, 'max_positions' => 12, 'max_weight' => 0.12, 'low_vol_bonus' => 0.05, 'cash_buffer' => 0.05, 'desc' => '收益风险均衡'],
        '进取' => ['target_vol' => 0.20, 'max_dd_derisk' => 0.25, 'max_positions' => 10, 'max_weight' => 0.15, 'low_vol_bonus' => 0.00, 'cash_buffer' => 0.02, 'desc' => '波动换收益，适度集中'],
        '激进' => ['target_vol' => 0.28, 'max_dd_derisk' => 0.35, 'max_positions' => 8,  'max_weight' => 0.20, 'low_vol_bonus' => -0.05, 'cash_buffer' => 0.00, 'desc' => '高波动容忍，集中持仓'],
    ];

    /** @var array 持有周期定义域 */
    public static $horizonLevels = [
        '短线' => ['rebalance_days' => 5,  'momentum_lookback' => 20,  'trend_lookback' => 20,  'reversal_weight' => 0.20, 'fwd_days' => 5,  'desc' => '周度调仓，短周期动量+反转'],
        '波段' => ['rebalance_days' => 10, 'momentum_lookback' => 60,  'trend_lookback' => 60,  'reversal_weight' => 0.10, 'fwd_days' => 10, 'desc' => '双周调仓，中短周期'],
        '中线' => ['rebalance_days' => 20, 'momentum_lookback' => 120, 'trend_lookback' => 120, 'reversal_weight' => 0.00, 'fwd_days' => 20, 'desc' => '月度调仓，经典中期动量'],
        '长线' => ['rebalance_days' => 60, 'momentum_lookback' => 180, 'trend_lookback' => 200, 'reversal_weight' => 0.00, 'fwd_days' => 60, 'desc' => '季度调仓，长周期趋势'],
        '半年' => ['rebalance_days' => 120, 'momentum_lookback' => 250, 'trend_lookback' => 250, 'reversal_weight' => 0.00, 'fwd_days' => 120, 'desc' => '半年调仓，年度级别趋势'],
    ];

    /** @var array 常用周期名 -> 内部 horizon 名（周5日/双周10日/月20日/季60日/半年120日） */
    public static $periodAliases = ['周' => '短线', '双周' => '波段', '月' => '中线', '季' => '长线', '半年' => '半年'];

    public static $defaultStyle = [
        'momentum' => 0.30, 'quality_momentum' => 0.15, 'low_vol' => 0.20,
        'trend' => 0.20, 'reversal' => 0.05, 'high_52w' => 0.05, 'liquidity' => 0.05,
    ];

    /**
     * 生成策略规格。
     *
     * @param string     $risk      保守/稳健/平衡/进取/激进
     * @param string     $horizon   短线/波段/中线/长线
     * @param array|null $style     因子权重覆盖，如 ['momentum'=>0.4,'low_vol'=>0.3,'trend'=>0.3]
     * @param array      $overrides 可选覆盖: max_positions/rebalance_days/cash_buffer/trend_filter
     * @return array 策略规格（可 json_encode）
     */
    public static function generate($risk = '平衡', $horizon = '波段', array $style = null, array $overrides = [])
    {
        if (isset(self::$periodAliases[$horizon])) {
            $horizon = self::$periodAliases[$horizon];
        }
        if (!isset(self::$riskLevels[$risk])) {
            throw new InvalidArgumentException('风险偏好必须为: ' . implode('/', array_keys(self::$riskLevels)));
        }
        if (!isset(self::$horizonLevels[$horizon])) {
            throw new InvalidArgumentException('持有周期必须为: ' . implode('/', array_keys(self::$horizonLevels)));
        }
        $r = self::$riskLevels[$risk];
        $h = self::$horizonLevels[$horizon];

        // 风格权重：默认 -> 风险偏好微调 -> 周期反转权重 -> 用户覆盖
        $styleW = self::$defaultStyle;
        $styleW['low_vol'] = max(0.0, $styleW['low_vol'] + $r['low_vol_bonus']);
        $styleW['reversal'] = $h['reversal_weight'];
        if ($style) {
            foreach ($style as $k => $v) {
                if (!array_key_exists($k, $styleW)) {
                    throw new InvalidArgumentException("未知因子: {$k}");
                }
                $styleW[$k] = (float)$v;
            }
        }
        $styleW = self::normalize($styleW);

        $lookbacks = [
            'momentum' => $h['momentum_lookback'],
            'quality_momentum' => 120,
            'reversal' => 5,
            'low_vol' => 60,
            'trend' => $h['trend_lookback'],
            'high_52w' => 250,
            'liquidity' => 20,
        ];

        return [
            'name' => isset($overrides['name']) ? $overrides['name'] : "{$risk}{$horizon}多因子",
            'generated_at' => date('Y-m-d H:i'),
            'preferences' => [
                'risk' => $risk, 'risk_desc' => $r['desc'],
                'horizon' => $horizon, 'horizon_desc' => $h['desc'],
            ],
            'selection' => [
                'factors' => $styleW,
                'factor_lookbacks' => $lookbacks,
                'max_positions' => isset($overrides['max_positions']) ? (int)$overrides['max_positions'] : $r['max_positions'],
                'min_history' => 120,
            ],
            'portfolio' => [
                'rebalance_days' => isset($overrides['rebalance_days']) ? (int)$overrides['rebalance_days'] : $h['rebalance_days'],
                'cash_buffer' => isset($overrides['cash_buffer']) ? (float)$overrides['cash_buffer'] : $r['cash_buffer'],
                'max_weight' => $r['max_weight'],
                'fwd_days' => $h['fwd_days'],
            ],
            'risk_control' => [
                'target_vol' => $r['target_vol'],
                'vol_lookback' => 60,
                'max_drawdown_derisk' => $r['max_dd_derisk'],
                'de_risk_ratio' => 0.5,
                'trend_filter' => !isset($overrides['trend_filter']) || $overrides['trend_filter'],
                'trend_filter_ma' => 200,
            ],
            'execution' => [
                'commission' => 0.0003, 'stamp_tax_sell' => 0.0005, 'slippage' => 0.001,
            ],
        ];
    }

    /** 归一化风格权重（过滤非正值，总和归一）。 */
    public static function normalize(array $w)
    {
        $filtered = [];
        foreach ($w as $k => $v) {
            if ((float)$v > 0) {
                $filtered[$k] = (float)$v;
            }
        }
        $total = array_sum($filtered);
        if ($total <= 0) {
            throw new InvalidArgumentException('风格权重总和必须为正');
        }
        $out = [];
        foreach ($filtered as $k => $v) {
            $out[$k] = $v / $total;
        }
        return $out;
    }
}
