提交
This commit is contained in:
116
backend/scripts/active_mv_probe.py
Normal file
116
backend/scripts/active_mv_probe.py
Normal file
@@ -0,0 +1,116 @@
|
||||
"""复刻指南针 0AMV(活跃市值)——可行性验证原型。
|
||||
|
||||
模型:
|
||||
活跃筹码比例(t) = 1 - Π_{近N个交易日} (1 - 换手率) (换手衰减)
|
||||
0AMV(t) = Σ_个股 流通市值(t) × 活跃筹码比例(t)
|
||||
流通市值不额外拉接口:成交量(股) / (换手%/100) 反推流通股本,× close 即得,
|
||||
且随解禁/增发每日自适应。N 为指南针未公开的窗口参数,输出多组候选供对照 app 校准。
|
||||
|
||||
用法(backend 目录):
|
||||
env -u SSLKEYLOGFILE uv run python scripts/active_mv_probe.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
|
||||
|
||||
WINDOWS = (20, 60, 120) # 活跃窗口候选(交易日)
|
||||
LOOKBACK_DAYS = 500 # 日历日回看,足够 120 交易日窗口
|
||||
SHOW_DAYS = 20 # 打印最近 N 个交易日
|
||||
|
||||
|
||||
async def _load() -> tuple[pd.DataFrame, pd.DataFrame]:
|
||||
async with async_session() as session:
|
||||
rows = (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()
|
||||
snap = (await session.execute(text("""
|
||||
SELECT ts_code, circ_mv, trade_date
|
||||
FROM daily_snapshot
|
||||
WHERE trade_date >= (SELECT max(trade_date) - interval '10 days' FROM daily_snapshot)
|
||||
AND circ_mv IS NOT NULL
|
||||
"""))).fetchall()
|
||||
df = pd.DataFrame(rows, columns=["ts", "symbol", "close", "volume", "amount", "turnover"])
|
||||
snap = pd.DataFrame(snap, columns=["ts_code", "circ_mv_wan", "snap_date"])
|
||||
return df, snap
|
||||
|
||||
|
||||
def _validate(df: pd.DataFrame, snap: pd.DataFrame) -> None:
|
||||
"""反推流通市值 vs daily_snapshot(万元→元)交叉验证。"""
|
||||
if snap.empty:
|
||||
print("校验:daily_snapshot 为空")
|
||||
return
|
||||
snap["symbol"] = snap["ts_code"].str.split(".").str[0]
|
||||
snap["d"] = snap["snap_date"].dt.date
|
||||
common = set(snap["d"]) & set(df["date"])
|
||||
if not common:
|
||||
print("校验:daily_snapshot 与 candles 无可对比日期")
|
||||
return
|
||||
snap_date = max(common)
|
||||
day = df[df["date"] == snap_date]
|
||||
m = day[["symbol", "circ_mv"]].merge(
|
||||
snap.loc[snap["d"] == snap_date, ["symbol", "circ_mv_wan"]],
|
||||
on="symbol", how="inner",
|
||||
)
|
||||
if m.empty:
|
||||
print("校验:无可对比个股")
|
||||
return
|
||||
ratio = m["circ_mv"] / (m["circ_mv_wan"] * 1e4)
|
||||
print(f"校验({snap_date},{len(m)} 只):反推/官方流通市值中位数 = {ratio.median():.4f},"
|
||||
f"P10={ratio.quantile(0.1):.3f},P90={ratio.quantile(0.9):.3f}")
|
||||
big = m.nlargest(5, "circ_mv")
|
||||
for _, r in big.iterrows():
|
||||
print(f" {r['symbol']}: 反推 {r['circ_mv']/1e8:,.0f}亿 vs 官方 {r['circ_mv_wan']/1e4:,.0f}亿"
|
||||
f"(比值 {r['circ_mv']/(r['circ_mv_wan']*1e4):.3f})")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
df, snap = asyncio.run(_load())
|
||||
df["date"] = df["ts"].dt.date
|
||||
# 关键:分组滚动前必须按 (symbol, date) 排序,SQL 返回顺序不保证
|
||||
df = df.sort_values(["symbol", "date"], kind="stable").reset_index(drop=True)
|
||||
print(f"candles 载入 {len(df):,} 行,{df['symbol'].nunique():,} 只,"
|
||||
f"{df['date'].min()} ~ {df['date'].max()}")
|
||||
|
||||
t = (df["turnover"] / 100.0).clip(upper=0.9999)
|
||||
df["float_shares"] = df["volume"] / t # 流通股本(股);volume 单位=股
|
||||
df["circ_mv"] = df["float_shares"] * df["close"] # 流通市值(元)
|
||||
|
||||
_validate(df, snap)
|
||||
|
||||
df["log_inactive"] = np.log1p(-t)
|
||||
agg_cols = {"circ_mv": "sum", "amount": "sum"}
|
||||
for n in WINDOWS:
|
||||
decay = df.groupby("symbol")["log_inactive"].transform(
|
||||
lambda s: s.rolling(n, min_periods=1).sum()
|
||||
)
|
||||
df[f"amv_{n}"] = df["circ_mv"] * (1.0 - np.exp(decay.to_numpy()))
|
||||
agg_cols[f"amv_{n}"] = "sum"
|
||||
|
||||
daily = df.groupby("date").agg(agg_cols).sort_index().tail(SHOW_DAYS)
|
||||
print("\n日期 全市场流通市值(万亿) 日成交额(万亿) " +
|
||||
" ".join(f"0AMV_{n}(万亿) 活跃占比_{n}" for n in WINDOWS))
|
||||
for d, r in daily.iterrows():
|
||||
print(f"{d} {r['circ_mv']/1e12:8.3f} {r['amount']/1e12:6.3f} " +
|
||||
" ".join(
|
||||
f"{r[f'amv_{n}']/1e12:7.3f} {r[f'amv_{n}']/r['circ_mv']*100:5.1f}%"
|
||||
for n in WINDOWS
|
||||
))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user