This commit is contained in:
2026-09-16 09:09:10 +08:00
parent 71a0f6e404
commit a490fdc110
8 changed files with 487 additions and 196 deletions

View 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 = Σ 自由流通市值 × active20active20 = 1-Π(1-流通换手) 滚动20日。
自由流通市值 = volume/(turnover_rate_f%) × closedaily_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()