<?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;
}
}