波动捕手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,

)

\

{link}