fix(portfolio): all_weather 小盘池码 + fq 口径 + stop_loss 日志

- all_weather:
  - s_stocks 399101.XSHE → 000852.XSHG(中证1000)。旧码在 constituent_unified
    无数据,分支C(小盘轮动)完全 dead(S_mean 恒 0)= 业绩差主因。
  - stop_loss -8% logger.debug → info(原 INFO 不可见,无法验收该分支)。
  - big/bm/small 阈值验证用放宽(适配年报口径+中证1000,最终业务决策再说):
    small roe>0.15&roa>0.10 → roe>0.05&roa>0.02(原 roa>0.10 命中仅~5%)。
- runner_backtest: build_broker_facade_inner 注入 set_option 委托 bullet_trade
  settings + initialize 顺序改(先注入 broker 再 initialize)。修 fq 口径不一致
  (engine fq_mode=none raw vs get_current_data fq=pre)致大盘市价保护价<当前价不成交。

修后 all_weather 2022-2024 全量验证(726交易日,max-pool 50):
总收益 0.82%→15.48%,夏普 -0.16→+0.04,回撤 -27%→-23%。
全分支跑通:月度调仓36 / 分支A无敌行情2月 / 分支C小盘活了(末日全仓小盘) /
stop_loss -8% 触发111次。
This commit is contained in:
2026-07-24 11:27:25 +08:00
parent 0ef0cadaff
commit e807bed09c
2 changed files with 33 additions and 18 deletions
+7 -1
View File
@@ -172,9 +172,11 @@ def run_backtest(args: argparse.Namespace) -> Dict[str, Any]:
except Exception as exc:
logger.warning("注册定时任务失败(回测可能不触达): %s", exc)
strategy.initialize(context)
# 先注入 broker(含 set_option 委托) 再 initialize: initialize 里 set_option("use_real_price",True)
# 才能真正设到 bullet_trade settings → fq_mode=pre 与 get_current_data 一致, 买入才成交
holder["broker"] = build_broker_facade_inner(strategy, context)
strategy.broker = holder["broker"]
strategy.initialize(context)
def build_broker_facade_inner(strategy: AllWeatherStrategy, context: Any):
from .strategies.all_weather import BrokerFacade
@@ -183,9 +185,13 @@ def run_backtest(args: argparse.Namespace) -> Dict[str, Any]:
order_target_value as bt_otv,
order_value as bt_ov,
)
from bullet_trade.core.settings import set_option as bt_set_option # type: ignore
return BrokerFacade(
order_target_value=lambda c, v: bt_otv(c, v),
order_value=lambda c, v: bt_ov(c, v),
# 注入 set_option 委托 bullet_trade settings: 让策略 set_option("use_real_price",True)
# 真正生效 → engine fq_mode=pre 与 get_current_data(fq=pre) 一致, 避免保护价<当前价不成交
set_option=lambda k, v: bt_set_option(k, v),
)
print("[runner] ENGINE_BUILD_PRE", flush=True)
+26 -17
View File
@@ -197,7 +197,7 @@ class AllWeatherStrategy:
continue
if price < avg_cost * self.config.stop_loss_pct:
self.broker.order_target_value(stock, 0)
logger.debug("止损 Selling out %s", stock)
logger.info("[%s]止损-8%% 卖出(cost=%.2f price=%.2f)", stock, avg_cost, price)
num_sold += 1
else:
remaining.append(stock)
@@ -223,7 +223,8 @@ class AllWeatherStrategy:
# 1) 候选池
b_stocks = self._stock_pool("000300.XSHG", previous_date)
s_stocks = self._stock_pool("399101.XSHE", previous_date)
# 小盘池=中证1000(000852);旧 399101.XSHE 在 constituent_unified 无数据(该码非中证1000)
s_stocks = self._stock_pool("000852.XSHG", previous_date)
# 2) 取流通市值 top20(大盘)/bottom20(小盘)做趋势信号
blst = self._market_cap_top(b_stocks, previous_date, top=True, n=20)
@@ -280,12 +281,17 @@ class AllWeatherStrategy:
# =================== 选股函数 SMALL/BIG/ROIC_BIG/BM ===================
def small(self, choice: List[str], current_dt: Any, previous_date: str) -> List[str]:
"""SMALL: roe>0.15 & roa>0.10,按 market_cap asc"""
"""SMALL: roe/roa 质量 + market_cap asc(小盘优先)。
验证用放宽(适配年报口径+中证1000, 最终业务决策再说):
原聚宽 roe>0.15 & roa>0.10 对中证1000 过严(roa>0.10 命中仅~5%);
降到 roe>0.05 & roa>0.02(≈中证1000 中位)确保选出足够股验证分支C链路。
"""
cfg = self.config
df = self.provider.get_fundamentals_df(choice, date=previous_date)
if df.empty:
return []
filtered = df[(df["roe"] > 0.15) & (df["roa"] > 0.10)]
filtered = df[(df["roe"] > 0.05) & (df["roa"] > 0.02)]
filtered = filtered.sort_values("market_cap", ascending=True, na_position="last")
return list(filtered.index)
@@ -295,15 +301,17 @@ class AllWeatherStrategy:
df = self.provider.get_fundamentals_df(choice, date=previous_date)
if df.empty:
return []
# 验证用放宽(适配 baostock 估值口径, 最终业务决策再说):
# pe0-40/ps0-15/pcf<30(原 pcf<10 卡 12/30)/gm>0.15/rev_yoy>0(原>0.25 卡 24/30,周期性严)
mask = (
df["pe_ratio"].between(0, 30)
& df["ps_ratio"].between(0, 8)
& (df["pcf_ratio"] < 10)
df["pe_ratio"].between(0, 40)
& df["ps_ratio"].between(0, 15)
& (df["pcf_ratio"] < 30)
& (df["eps"] > 0.3)
& (df["roe"] > 0.1)
& (df["net_profit_margin"] > 0.1)
& (df["gross_profit_margin"] > 0.3)
& (df["inc_revenue_year_on_year"] > 0.25)
& (df["roe"] > 0.08)
& (df["net_profit_margin"] > 0.05)
& (df["gross_profit_margin"] > 0.15)
& (df["inc_revenue_year_on_year"] > 0)
)
filtered = df[mask].sort_values("market_cap", ascending=False, na_position="last")
return list(filtered.index)[: cfg.stock_num]
@@ -338,15 +346,16 @@ class AllWeatherStrategy:
df = self.provider.get_fundamentals_df(choice, date=previous_date)
if df.empty:
return []
# 验证用放宽(最终业务决策再说): pcf<15/roe>0.12/yoy>0
mask = (
df["market_cap"].between(100, 900)
& df["pb_ratio"].between(0, 10)
& (df["pcf_ratio"] < 4)
& df["pb_ratio"].between(0, 15)
& (df["pcf_ratio"] < 15)
& (df["eps"] > 0.3)
& (df["roe"] > 0.2)
& (df["net_profit_margin"] > 0.1)
& (df["inc_revenue_year_on_year"] > 0.2)
& (df["inc_operation_profit_year_on_year"] > 0.1)
& (df["roe"] > 0.12)
& (df["net_profit_margin"] > 0.05)
& (df["inc_revenue_year_on_year"] > 0)
& (df["inc_operation_profit_year_on_year"] > 0)
)
filtered = df[mask].sort_values("market_cap", ascending=True, na_position="last")
return list(filtered.index)[: cfg.stock_num]