提交
This commit is contained in:
152
backend/scripts/active_mv_calib.py
Normal file
152
backend/scripts/active_mv_calib.py
Normal file
@@ -0,0 +1,152 @@
|
||||
"""0AMV 校验 v2:对照指南针 app EOD 收盘读数,寻找能否精确一致。
|
||||
|
||||
指南针目标(亿元,app 收盘读数):
|
||||
2026-09-11: 172,364.3
|
||||
2026-09-14: 169,908.5(开 171,979.5 高 175,413.6 低 169,908.5)
|
||||
2026-09-15: 166,791.5(开 169,782.7 高 173,060.4 低 166,658.0)
|
||||
|
||||
模型:0AMV = Σ 自由流通市值 × active20,active20 = 1-Π(1-流通换手) 滚动20日。
|
||||
自由流通市值 = volume/(turnover_rate_f%) × close(daily_snapshot 近期才有 tff)。
|
||||
检验变体:全市场 / 剔北交所 / 再剔科创板 / 剔次新(上市<90自然日),
|
||||
若均差一个常数因子 → 指南针自由流通股本口径私有,无法精确复刻。
|
||||
|
||||
用法(backend 目录):
|
||||
env -u SSLKEYLOGFILE PYTHONIOENCODING=utf-8 uv run python scripts/active_mv_calib.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from sqlalchemy import text
|
||||
|
||||
from app.db import async_session
|
||||
|
||||
N_WIN = 20
|
||||
TARGETS = {"2026-09-11": 172364.3e8, "2026-09-14": 169908.5e8, "2026-09-15": 166791.5e8}
|
||||
|
||||
|
||||
async def _load() -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame, pd.DataFrame]:
|
||||
async with async_session() as session:
|
||||
hist = (await session.execute(text("""
|
||||
SELECT ts, symbol, close, volume, amount, turnover
|
||||
FROM candles
|
||||
WHERE timeframe = '1d'
|
||||
AND turnover IS NOT NULL AND turnover > 0
|
||||
AND volume > 0
|
||||
AND ts >= now() - interval '500 days'
|
||||
"""))).fetchall()
|
||||
pend = (await session.execute(text("""
|
||||
SELECT ts, symbol, close, volume, amount
|
||||
FROM candles
|
||||
WHERE timeframe = '1d' AND volume > 0
|
||||
AND ts::date = (SELECT max(ts)::date FROM candles
|
||||
WHERE timeframe='1d' AND volume > 0)
|
||||
"""))).fetchall()
|
||||
snap = (await session.execute(text("""
|
||||
SELECT ts_code, trade_date::date AS d, turnover_rate, turnover_rate_f
|
||||
FROM daily_snapshot
|
||||
WHERE trade_date >= now() - interval '20 days'
|
||||
AND turnover_rate IS NOT NULL AND turnover_rate_f > 0
|
||||
"""))).fetchall()
|
||||
basic = (await session.execute(text("""
|
||||
SELECT symbol, market, exchange, list_date FROM stock_basic
|
||||
"""))).fetchall()
|
||||
return (
|
||||
pd.DataFrame(hist, columns=["ts", "symbol", "close", "volume", "amount", "turnover"]),
|
||||
pd.DataFrame(pend, columns=["ts", "symbol", "close", "volume", "amount"]),
|
||||
pd.DataFrame(snap, columns=["ts_code", "d", "turnover", "turnover_rate_f"]),
|
||||
pd.DataFrame(basic, columns=["symbol", "market", "exchange", "list_date"]),
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
hist, pend, snap, basic = asyncio.run(_load())
|
||||
snap["symbol"] = snap["ts_code"].str.split(".").str[0]
|
||||
|
||||
# 最新 candles 交易日若缺换手率(夜间 3.5 步未跑),用 snapshot 补
|
||||
if not pend.empty and not snap.empty:
|
||||
d_pend = pend["ts"].dt.date.max()
|
||||
sp = snap[snap["d"] == d_pend][["symbol", "turnover"]]
|
||||
pend = pend[pend["ts"].dt.date == d_pend].merge(sp, on="symbol", how="inner")
|
||||
pend = pend[pend["turnover"] > 0]
|
||||
print(f"最新交易日 {d_pend}:{len(pend):,} 行用 snapshot 换手率补齐")
|
||||
|
||||
df = pd.concat([hist, pend[hist.columns]], ignore_index=True)
|
||||
df["date"] = df["ts"].dt.date
|
||||
df = df.sort_values(["symbol", "date"], kind="stable").reset_index(drop=True)
|
||||
df = df.merge(basic, on="symbol", how="left")
|
||||
print(f"合计 {len(df):,} 行,{df['symbol'].nunique():,} 只,{df['date'].min()} ~ {df['date'].max()}")
|
||||
print("板块分布:", df.drop_duplicates("symbol")["market"].value_counts().to_dict())
|
||||
|
||||
t = (df["turnover"] / 100.0).clip(upper=0.9999)
|
||||
df["log_inactive"] = np.log1p(-t)
|
||||
decay = df.groupby("symbol")["log_inactive"].transform(
|
||||
lambda s: s.rolling(N_WIN, min_periods=1).sum()
|
||||
)
|
||||
df["active20"] = 1.0 - np.exp(decay.to_numpy())
|
||||
|
||||
# ---- 每个有 tff 的日期:个股级 FF20 明细 ----
|
||||
ms: dict = {}
|
||||
for d in sorted(snap["d"].unique())[-12:]:
|
||||
day = df[df["date"] == d]
|
||||
if day.empty:
|
||||
continue
|
||||
m = day.merge(snap.loc[snap["d"] == d, ["symbol", "turnover_rate_f"]],
|
||||
on="symbol", how="inner")
|
||||
m = m[m["turnover_rate_f"] > 0]
|
||||
tff = (m["turnover_rate_f"] / 100.0).clip(upper=0.9999)
|
||||
m["free_mv"] = m["volume"] / tff * m["close"]
|
||||
m["ff"] = m["free_mv"] * m["active20"]
|
||||
listed = pd.to_datetime(m["list_date"], format="%Y%m%d", errors="coerce")
|
||||
m["age_days"] = (pd.Timestamp(d) - listed).dt.days.fillna(10**6)
|
||||
ms[d] = m
|
||||
|
||||
# ---- 参数扫描:剔除次新窗口 × 是否剔北交所 ----
|
||||
print("\n== 参数扫描(对指南针三日的比值;理想=1.00000 稳定)==")
|
||||
print(f"{'剔北交所':<6}{'剔次新(自然日)':>12}{'09-11':>10}{'09-14':>10}{'09-15':>10}{'均值':>10}{'极差':>9}")
|
||||
results = []
|
||||
for bse_ex in (False, True):
|
||||
for w in (0, 60, 90, 120, 150, 180, 270, 365, 550):
|
||||
ratios = []
|
||||
for tgt_d, tgt in TARGETS.items():
|
||||
d = pd.Timestamp(tgt_d).date()
|
||||
if d not in ms:
|
||||
break
|
||||
m = ms[d]
|
||||
mask = m["age_days"] >= w
|
||||
if bse_ex:
|
||||
mask &= m["market"] != "北交所"
|
||||
ratios.append(m.loc[mask, "ff"].sum() / tgt)
|
||||
if len(ratios) < 3:
|
||||
continue
|
||||
mean_r = float(np.mean(ratios))
|
||||
spread = max(ratios) - min(ratios)
|
||||
results.append((abs(mean_r - 1) + spread, bse_ex, w, ratios, mean_r, spread))
|
||||
print(f"{'是' if bse_ex else '否':<6}{w:>12}{ratios[0]:>10.5f}{ratios[1]:>10.5f}"
|
||||
f"{ratios[2]:>10.5f}{mean_r:>10.5f}{spread:>9.5f}")
|
||||
|
||||
results.sort()
|
||||
_, best_bse, best_w, best_ratios, best_mean, best_spread = results[0]
|
||||
print(f"\n最优配置:剔北交所={'是' if best_bse else '否'},剔上市<{best_w}自然日")
|
||||
print(f" 三日比值 {['%.5f' % r for r in best_ratios]},极差 {best_spread:.5f}")
|
||||
|
||||
# 最优配置下的每日序列
|
||||
print("\n== 最优配置近 12 日 FF20(亿)==")
|
||||
for d in sorted(ms):
|
||||
m = ms[d]
|
||||
mask = m["age_days"] >= best_w
|
||||
if best_bse:
|
||||
mask &= m["market"] != "北交所"
|
||||
v = m.loc[mask, "ff"].sum() / 1e8
|
||||
mark = f" ←指南针 {TARGETS[str(d)]/1e8:,.1f}" if str(d) in TARGETS else ""
|
||||
print(f"{d} {v:>10,.0f} n={int(mask.sum())}{mark}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user