akg-factor-bridge/common.py

160 lines
7.3 KiB
Python
Raw Normal View History

2026-07-27 09:49:05 +08:00
"""共用:股票码规范化、覆盖池、交易日历、幂等写因子表、注册 factor_metadata自适应列"""
2026-07-24 14:20:54 +08:00
import json
import pandas as pd
2026-07-27 09:49:05 +08:00
import config
2026-07-24 14:20:54 +08:00
import db
def to_prefix(ts_code: str) -> str:
"""600000.SH -> SH600000已是前缀式(SZ002625)或纯代码则原样返回。"""
s = str(ts_code).strip()
if "." in s:
num, exch = s.split(".", 1)
return f"{exch.upper()}{num}"
return s
def load_universe() -> set[str]:
2026-07-27 09:49:05 +08:00
"""KG 覆盖池 ts_code 集合600000.SH 形态),来自基座视图 v_factor_universe。
这是**当前态**industry_pools 只存最新快照每周一 refresh_pools 自动长大
用它过滤历史数据会引入成员性前视 universe_filter config.SUBFACTOR_UNIVERSE
"""
2026-07-24 14:20:54 +08:00
df = db.read_pg("SELECT ts_code FROM v_factor_universe")
return set(df["ts_code"].astype(str).str.strip())
2026-07-27 09:49:05 +08:00
def universe_filter(df: pd.DataFrame, col: str = "stock_code",
as_prefix: bool = True) -> pd.DataFrame:
"""按 config.SUBFACTOR_UNIVERSE 决定子因子是否受覆盖池限制。
'pool'默认保持原行为只留池内成员
'market'评审 §4 建议全市场出行池只在 akg_score 侧当过滤器
为什么建议 marketindustry_pools 只存最新态且每周一长大用它过滤历史子因子
值是点时正确的**成员性却是前视的**用今天的池去筛历史而今天的池里有大量
主题是后来的研报才长出来的正是 §2.2 批评传导时用的同一条论证
同时截面统计量会在每周一发生结构性跳变IC 序列带周期性伪影
子因子表是可独立观察的仪表过滤该发生在消费端
"""
if config.SUBFACTOR_UNIVERSE == "pool":
uni = load_universe()
if as_prefix:
uni = {to_prefix(x) for x in uni}
return df[df[col].astype(str).str.strip().isin(uni)]
return df
2026-07-24 14:20:54 +08:00
def trading_days(start: str, end: str) -> list:
2026-07-27 09:49:05 +08:00
"""目标区间交易日历。
曾用热度表 stock_fund_heat_scores 但它最早只有 2025-03-26
导致 `build akg_event --mode history --start 2024-01-01`README 里的示例命令
拿到空日历 静默返回空 只打印无数据跳过不报错
改用行情表 gp_day_data实测 5584 覆盖全历史
"""
df = db.read_mysql(
"price",
"SELECT DISTINCT `timestamp` AS trade_date FROM gp_day_data "
"WHERE `timestamp` BETWEEN %s AND %s ORDER BY `timestamp`",
(start, end))
if df.empty:
print(f" ⚠️ 交易日历为空gp_day_data 在 {start}~{end} 无数据)"
f"——上层会跳过该区间,请核对区间与行情表覆盖")
2026-07-24 14:20:54 +08:00
return list(pd.to_datetime(df["trade_date"]))
_CREATE_FACTOR_TABLE = """
CREATE TABLE IF NOT EXISTS {t} (
trade_date DATE NOT NULL,
stock_code VARCHAR(15) NOT NULL,
factor_value DOUBLE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (trade_date, stock_code),
KEY idx_stock (stock_code)
) ENGINE=InnoDB
"""
def ensure_table(table: str) -> None:
with db.factor_conn() as conn:
with conn.cursor() as cur:
cur.execute(_CREATE_FACTOR_TABLE.format(t=table))
conn.commit()
2026-07-27 09:49:05 +08:00
def write_factor(table: str, df: pd.DataFrame, mode: str = "daily") -> None:
2026-07-24 14:20:54 +08:00
"""df 列 [trade_date, stock_code, factor_value] → 幂等写因子表。
2026-07-27 09:49:05 +08:00
daily/history 都是删涉及日期区间 批插stock_code 统一转前缀式
分块提交评审热度全史约 54 万行原来单事务 executemany ShardingSphere
代理有风险改成 DELETE 一个事务 + INSERT config.WRITE_CHUNK_ROWS 分块提交
代价是中途失败会留下部分区间但整段写入按日期区间幂等重跑即修复
"""
2026-07-24 14:20:54 +08:00
if df is None or df.empty:
print(f" {table}: 无数据(跳过)")
return
df = df.dropna(subset=["trade_date", "stock_code", "factor_value"]).copy()
df["stock_code"] = df["stock_code"].map(to_prefix)
df["trade_date"] = pd.to_datetime(df["trade_date"]).dt.date
df = df.drop_duplicates(["trade_date", "stock_code"], keep="last")
if df.empty:
print(f" {table}: 清洗后无数据")
return
dmin, dmax = df["trade_date"].min(), df["trade_date"].max()
ensure_table(table)
rows = list(df[["trade_date", "stock_code", "factor_value"]]
.itertuples(index=False, name=None))
2026-07-27 09:49:05 +08:00
chunk = max(1, config.WRITE_CHUNK_ROWS)
2026-07-24 14:20:54 +08:00
with db.factor_conn() as conn:
with conn.cursor() as cur:
2026-07-27 09:49:05 +08:00
cur.execute(f"DELETE FROM {table} WHERE trade_date BETWEEN %s AND %s",
(dmin, dmax))
2026-07-24 14:20:54 +08:00
conn.commit()
2026-07-27 09:49:05 +08:00
for i in range(0, len(rows), chunk):
with conn.cursor() as cur:
cur.executemany(
f"INSERT INTO {table} (trade_date, stock_code, factor_value) "
f"VALUES (%s,%s,%s)", rows[i:i + chunk])
conn.commit()
if len(rows) > chunk:
print(f" ...{min(i + chunk, len(rows))}/{len(rows)}")
2026-07-24 14:20:54 +08:00
print(f" {table}: 写入 {len(rows)} 行, 日期 {dmin}~{dmax}")
def register(factor_code, display_name, table, category, desc,
author="akg-factor-bridge") -> None:
"""注册 factor_metadata。自适应实际存在的列避免猜死 schema——
列名以线上 factor_metadata 为准本函数只写它有的列"""
values = {
"factor_code": factor_code, "display_name": display_name,
"target_ds_name": "ds_a", "target_table_name": table,
"author": author, "description": desc,
"category": json.dumps(category, ensure_ascii=False),
"frequency": "daily", "status": "active", "factor_type": "single",
}
with db.factor_conn() as conn:
with conn.cursor() as cur:
2026-07-24 14:57:11 +08:00
# ShardingSphere 代理不支持 information_schema 内省 → 用真实查询取列名
cur.execute("SELECT * FROM factor_metadata LIMIT 0")
cols = [d[0] for d in (cur.description or [])]
if not cols: # 极端兜底:代理连列元数据都不给时,用平台已知 schema
cols = ["factor_code", "display_name", "target_ds_name", "target_table_name",
"start_date", "end_date", "author", "description", "category",
"frequency", "status", "factor_type"]
2026-07-24 14:20:54 +08:00
use = [k for k in values if k in cols]
if "factor_code" not in use:
2026-07-24 14:57:11 +08:00
raise RuntimeError(f"factor_metadata 取不到 factor_code 列;实际列: {cols}")
2026-07-24 14:20:54 +08:00
ph = ",".join(["%s"] * len(use))
upd = ",".join(f"{k}=VALUES({k})" for k in use if k != "factor_code")
cur.execute(
f"INSERT INTO factor_metadata ({','.join(use)}) VALUES ({ph}) "
f"ON DUPLICATE KEY UPDATE {upd}", [values[k] for k in use])
conn.commit()
print(f" 注册 {factor_code} -> {table}(写入列: {sorted(use)}")
if "factor_type" not in cols:
print(" ⚠️ factor_metadata 无 factor_type 列——平台 /mining/factors/all 按 "
"factor_type IN('single','multiple') 过滤,缺列可能查不到本因子,请核实。")