波动捕手8172
由bqglvvuu创建,最终由bqglvvuu 被浏览 1 用户
"""价值因子选股:月末信号日收盘后用 jiazhi 表打分,T+1 开盘买入得分最高的 HOLD 只,等权,月度调仓。
Shape: cn_stock, 1d, stock, rebalance
股票池:全A,剔除 ST、停牌、上市不足 MIN_LIST_DAYS 个交易日、近 20 日日均成交额低于 MIN_AMOUNT;方向由 rule_card.json 锁定(DIRECTION=1,因子越大越好,详见第一步报告)。
不含 c_* 的过滤放在不含窗口函数的子查询里,窗口函数(20 日均额)先算完整窗口,行级淘汰条件放在外层。
"""
import datetime
import pandas as pd
from bigquant import bigtrader, dai
from bigquant.bigtrader import param
START = param("2019-01-01")
END = param("2024-04-30") # 训练+验证段结束日;第四步开测试集时改成测试段结束日
DIRECTION = param(1, choices=[1, -1], doc={"zh": "因子方向,由第一步训练段 IC 符号锁定,不作为调参对象", "en": "Factor direction, locked by step 1, never searched"})
HOLD = param(20, min=5, max=60, doc={"zh": "持有股票数,等权", "en": "Names held, equal weight"})
REBAL_MONTHS = param(1, min=1, max=6, doc={"zh": "调仓周期(月),1=每月月末调仓", "en": "Rebalance every N calendar months, on the month's last trading day"})
MAX_WEIGHT = param(0.08, min=0.01, max=1.0, doc={"zh": "单票权重上限", "en": "Max weight per name"})
BUDGET = param(0.95, min=0.5, max=1.0, step=0.01, doc={"zh": "投入组合市值比例,留出滑点和佣金空间", "en": "Share of portfolio value invested"})
MIN_LIST_DAYS = param(250, min=0, max=1000, doc={"zh": "上市交易日数下限", "en": "Minimum trading days since listing"})
MIN_AMOUNT = param(5e7, min=0.0, max=5e9, doc={"zh": "近20日日均成交额下限(元)", "en": "20-day average turnover floor, yuan"})
BUY_COMMISSION = param(0.0003, min=0.0, max=0.01, step=0.0001, doc={"zh": "买入佣金率;万三", "en": "Buy commission ratio"})
SELL_COMMISSION = param(0.0003, min=0.0, max=0.01, step=0.0001, doc={"zh": "卖出佣金率,不含印花税", "en": "Sell commission ratio, excluding stamp tax"})
MIN_COMMISSION = param(5.0, min=0.0, max=100.0, doc={"zh": "每笔最低佣金(元)", "en": "Minimum commission per order, yuan"})
STAMP_TAX = param(0.001, min=0.0, max=0.01, step=0.0001, doc={"zh": "卖出印花税率", "en": "Sell stamp tax ratio"})
SLIPPAGE = param(0.001, min=0.0, max=0.01, step=0.0001, doc={"zh": "单边滑点比例", "en": "Slippage per side, fraction"})
BENCHMARK = param("000985.CSI", doc={"zh": "中证全指:口径最接近全A剔除ST/停牌/次新/低流动性后的样本", "en": "CSI All Share, closest broad benchmark"})
FACTOR_EXPR = "f.jiazhi"
RANK_SQL = f"""
WITH px AS (
SELECT date, instrument, m_avg(amount, 20) AS amt20 FROM cn_stock_bar1d
),
j AS (
SELECT p.date, p.instrument, {FACTOR_EXPR} AS fac, f.sw2021_level1 AS ind, p.amt20,
s.st_status, s.suspended, COALESCE(b.trading_days, b.list_days \* 245 / 365) AS td
FROM px p
LEFT JOIN jiazhi f ON p.date = f.date AND p.instrument = f.instrument
LEFT JOIN cn_stock_status s ON p.date = s.date AND p.instrument = s.instrument
LEFT JOIN cn_stock_factors_base b ON p.date = b.date AND p.instrument = b.instrument
QUALIFY p.date = max(p.date) OVER (PARTITION BY date_trunc('month', p.date))
),
elig AS (
SELECT \*, CASE WHEN st_status = 0 AND suspended = 0 AND td >= {{min_days}} AND amt20 >= {{min_amt:g}} THEN 1 ELSE 0 END AS ok
FROM j
),
scored AS (
SELECT date, instrument,
row_number() OVER (PARTITION BY date ORDER BY {{direction}} \* CASE WHEN ok = 1 THEN fac END DESC NULLS LAST, instrument) AS rk
FROM elig
WHERE isfinite(fac)
)
SELECT date, instrument FROM scored QUALIFY rk <= {{hold}} ORDER BY date, instrument
"""
def ranks_sql():
return RANK_SQL.format(min_days=MIN_LIST_DAYS, min_amt=MIN_AMOUNT, direction=DIRECTION, hold=HOLD)
def period_end_ranks(start_date, end_date):
"""每个月末信号日得分最高的 HOLD 只;从 90 天前开始,让 20 日均额窗口有预热。"""
start = datetime.date.fromisoformat(start_date) - datetime.timedelta(days=90)
return dai.query(ranks_sql(), filters={"date": \[str(start), end_date\]}).df()
def universe(start_date, end_date):
return sorted(set(period_end_ranks(start_date, end_date)\["instrument"\]))
def initialize(context: bigtrader.Context):
context.set_commission(buy_ratio=BUY_COMMISSION, sell_ratio=SELL_COMMISSION, min_commission=MIN_COMMISSION, tax_ratio=STAMP_TAX)
context.set_slippage(SLIPPAGE, percent=True)
context.df = period_end_ranks(context.start_date, context.end_date)
context.sig_dates = sorted(context.df\["date"\].unique())
context.sig_idx = {d: i for i, d in enumerate(context.sig_dates)}
def is_rebalance_day(context, today_ts):
"""每月最后一个交易日,且与上一次调仓相隔 >= REBAL_MONTHS 个日历月。"""
if today_ts not in context.sig_idx:
return False
i = context.sig_idx\[today_ts\]
last = context.user_store.get("last_rebal_idx", -10\*\*9)
return i - last >= REBAL_MONTHS
def handle_data(context: bigtrader.Context, data: bigtrader.DataReader):
today = context.trading_day
today_ts = pd.Timestamp(today)
if not is_rebalance_day(context, today_ts):
return
context.user_store\["last_rebal_idx"\] = context.sig_idx\[today_ts\]
picks = set(context.df.loc\[context.df\["date"\] == today_ts, "instrument"\])
if not picks:
context.logger.warning("no ranks for today", today=today)
return
w = min(BUDGET / len(picks), MAX_WEIGHT)
targets = {inst: (w if inst in picks else 0.0) for inst in context.instruments}
value = context.get_fund_value("portfolio_value")
for inst in sorted(targets, key=lambda i: targets\[i\] - context.get_position(i).market_value / value):
context.order_target_percent(inst, targets\[inst\])
perf = bigtrader.run(
instruments=universe,
start_date=START,
end_date=END,
benchmark=BENCHMARK,
capital_base=1000000.0,
raise_on_error="warn",
initialize=initialize,
handle_data=handle_data,
)
\