166 lines
7.3 KiB
Python
166 lines
7.3 KiB
Python
"""四路子因子构造。输入日期区间 [start, end](YYYY-MM-DD),输出
|
||
DataFrame[trade_date, stock_code, factor_value],全部限制在 KG 覆盖池 universe 内。
|
||
stock_code 输出形态不限,common.write_factor 统一转前缀式。
|
||
|
||
建模参数(EVENT_POLARITY / *_HALF_LIFE / *_WINDOW)是**因子决策**,见设计文档
|
||
§3.3 与 §8-3,此处取草案默认,待用户确认后调。方向(direction)统一在合成配置里声明,
|
||
子因子表只存原始值——但 event 天然带符号,故此处保留极性。
|
||
"""
|
||
import numpy as np
|
||
import pandas as pd
|
||
|
||
import config
|
||
import common
|
||
import db
|
||
|
||
FACTORS = {
|
||
"akg_upside": "t_factor_akg_upside",
|
||
"akg_heat": "t_factor_akg_heat",
|
||
"akg_event": "t_factor_akg_event",
|
||
"akg_transmission": "t_factor_akg_transmission",
|
||
}
|
||
|
||
# ---- 事件极性草案(§3.3,待 §8-3 确认)----
|
||
EVENT_POLARITY = {
|
||
"股份回购": 1.0, "重大合同中标": 1.0, "股权激励授予": 0.5,
|
||
"诉讼仲裁": -1.0, "行政处罚": -1.0, "股权质押": -0.5,
|
||
"发行上市": 0.0, "并购交割": 0.0, "other": 0.0,
|
||
# 增减持 / 业绩预告:符号取决于 direction(下 EVENT_DIR)
|
||
"增减持": 0.0, "业绩预告": 0.0,
|
||
}
|
||
EVENT_DIR = {"预增": 1.0, "预减": -1.0, "增持": 1.0, "减持": -1.0}
|
||
EVENT_HALF_LIFE = 10 # 交易日
|
||
EVENT_WINDOW = 60 # 交易日(超窗不计)
|
||
_EMPTY = pd.DataFrame(columns=["trade_date", "stock_code", "factor_value"])
|
||
|
||
|
||
# ---------------------------------------------------------------- 热度
|
||
def build_heat(start, end):
|
||
"""热度 = stock_fund_heat_scores 最新批次 score(0~1)。stock_code 已前缀式。"""
|
||
uni = {common.to_prefix(x) for x in common.load_universe()}
|
||
df = db.read_mysql("heat",
|
||
"""SELECT s.trade_date, s.stock_code, s.score AS factor_value
|
||
FROM stock_fund_heat_scores s
|
||
JOIN (SELECT trade_date, MAX(batch_no) bn FROM stock_fund_heat_scores
|
||
WHERE trade_date BETWEEN %s AND %s GROUP BY trade_date) m
|
||
ON m.trade_date = s.trade_date AND m.bn = s.batch_no""",
|
||
(start, end))
|
||
if df.empty:
|
||
return _EMPTY
|
||
df["stock_code"] = df["stock_code"].astype(str).str.strip()
|
||
df["factor_value"] = pd.to_numeric(df["factor_value"], errors="coerce")
|
||
df = df[df["stock_code"].isin(uni)]
|
||
return df[["trade_date", "stock_code", "factor_value"]]
|
||
|
||
|
||
# ---------------------------------------------------------------- 预期空间
|
||
def build_upside(start, end):
|
||
"""upside = 一致预期目标价中枢 / 当日现价 − 1(as-of:现价日取 asof<=当日最新一致预期)。
|
||
现价来自平台 gp_day_data(代码列 config.PRICE_CODE_COL,待实机核实)。"""
|
||
uni = common.load_universe()
|
||
cons = db.read_pg(
|
||
"SELECT ts_code, asof_date, target_mid_avg FROM v_factor_consensus WHERE asof_date <= %s",
|
||
(end,))
|
||
cons = cons[cons["ts_code"].isin(uni)].copy()
|
||
if cons.empty:
|
||
return _EMPTY
|
||
col = config.PRICE_CODE_COL
|
||
price = db.read_mysql("price",
|
||
f"SELECT `timestamp` AS trade_date, `{col}` AS ts_code, close "
|
||
f"FROM gp_day_data WHERE `timestamp` BETWEEN %s AND %s", (start, end))
|
||
if price.empty:
|
||
return _EMPTY
|
||
price["close"] = pd.to_numeric(price["close"], errors="coerce")
|
||
# 归一到前缀式两边对齐(gp_day_data 代码形态不定 → 都折前缀式后 join)
|
||
price["k"] = price["ts_code"].map(common.to_prefix)
|
||
price = price[(price["close"] > 0)].dropna(subset=["close"])
|
||
cons["k"] = cons["ts_code"].map(common.to_prefix)
|
||
cons = cons[cons["k"].isin(set(price["k"]))]
|
||
if cons.empty:
|
||
return _EMPTY
|
||
cons["asof_date"] = pd.to_datetime(cons["asof_date"])
|
||
price["trade_date"] = pd.to_datetime(price["trade_date"])
|
||
out = []
|
||
cons_sorted = cons.sort_values("asof_date")
|
||
for k, pg in price.groupby("k"):
|
||
cg = cons_sorted[cons_sorted["k"] == k]
|
||
if cg.empty:
|
||
continue
|
||
m = pd.merge_asof(pg.sort_values("trade_date"),
|
||
cg[["asof_date", "target_mid_avg"]],
|
||
left_on="trade_date", right_on="asof_date", direction="backward")
|
||
m = m.dropna(subset=["target_mid_avg"])
|
||
if m.empty:
|
||
continue
|
||
m["factor_value"] = m["target_mid_avg"].astype(float) / m["close"] - 1.0
|
||
m["stock_code"] = k
|
||
out.append(m[["trade_date", "stock_code", "factor_value"]])
|
||
return pd.concat(out) if out else _EMPTY
|
||
|
||
|
||
# ---------------------------------------------------------------- 事件
|
||
def _polarity(event_type, direction):
|
||
d = (direction or "").strip()
|
||
if d in EVENT_DIR:
|
||
return EVENT_DIR[d]
|
||
return EVENT_POLARITY.get(event_type, 0.0)
|
||
|
||
|
||
def build_event(start, end):
|
||
"""事件分 = Σ 近窗口内事件 极性 × 时间衰减(exp(-交易日龄·ln2/半衰期))。
|
||
ts_code 取文档锚(v_factor_events 已解析)。无事件的股当天不出行(= 缺 → 合成侧填 0)。"""
|
||
uni = common.load_universe()
|
||
look = (pd.Timestamp(start) - pd.Timedelta(days=EVENT_WINDOW * 2)).date()
|
||
ev = db.read_pg(
|
||
"SELECT ts_code, disclosure_date, event_type, direction "
|
||
"FROM v_factor_events WHERE disclosure_date BETWEEN %s AND %s", (look, end))
|
||
ev = ev[ev["ts_code"].isin(uni)].copy()
|
||
if ev.empty:
|
||
return _EMPTY
|
||
ev["pol"] = [_polarity(t, d) for t, d in zip(ev["event_type"], ev["direction"])]
|
||
ev = ev[ev["pol"] != 0.0]
|
||
if ev.empty:
|
||
return _EMPTY
|
||
cal = common.trading_days(start, end)
|
||
if not cal:
|
||
return _EMPTY
|
||
cal = pd.DatetimeIndex(cal)
|
||
decay = np.log(2) / EVENT_HALF_LIFE
|
||
rows = []
|
||
for ts, g in ev.groupby("ts_code"):
|
||
disc = pd.to_datetime(g["disclosure_date"]).values.astype("datetime64[ns]")
|
||
pol = g["pol"].to_numpy(dtype=float)
|
||
for d in cal:
|
||
# 自然日龄 → 交易日龄近似 ×(5/7)(v1 近似,见 README 待优化项)
|
||
age_td = ((d.value - disc.astype("int64")) / 86_400e9) * (5.0 / 7.0)
|
||
mask = (age_td >= 0) & (age_td <= EVENT_WINDOW)
|
||
if not mask.any():
|
||
continue
|
||
val = float((pol[mask] * np.exp(-age_td[mask] * decay)).sum())
|
||
if val != 0.0:
|
||
rows.append((d.date(), ts, val))
|
||
return pd.DataFrame(rows, columns=["trade_date", "stock_code", "factor_value"]) if rows else _EMPTY
|
||
|
||
|
||
# ---------------------------------------------------------------- 传导
|
||
def build_transmission(start, end):
|
||
"""传导分 = 指向该股所在环节的路径数 ×(1 − 已动比例);同股同日多候选取最大。"""
|
||
uni = common.load_universe()
|
||
tr = db.read_pg(
|
||
"SELECT scan_date, ts_code, n_paths, moved_ratio "
|
||
"FROM v_factor_transmission WHERE scan_date BETWEEN %s AND %s", (start, end))
|
||
tr = tr[tr["ts_code"].isin(uni)].copy()
|
||
if tr.empty:
|
||
return _EMPTY
|
||
tr["factor_value"] = (tr["n_paths"].astype(float)
|
||
* (1.0 - pd.to_numeric(tr["moved_ratio"], errors="coerce").fillna(0.0)))
|
||
g = (tr.groupby(["scan_date", "ts_code"])["factor_value"].max().reset_index()
|
||
.rename(columns={"scan_date": "trade_date", "ts_code": "stock_code"}))
|
||
return g[["trade_date", "stock_code", "factor_value"]]
|
||
|
||
|
||
BUILDERS = {
|
||
"akg_upside": build_upside, "akg_heat": build_heat,
|
||
"akg_event": build_event, "akg_transmission": build_transmission,
|
||
}
|