Files
MoFin/deploy/profile-scripts/market_regime.py
T
hmo 21401f91cf 实盘补齐大盘市场阶段判断(market_regime): 与回测_load_index_ctx同算法
- 新增 market_regime.py: sh000001 的 above_ma20/ma20_slope/roc/adx 计算+regime分类(trend_up/choppy/trend_down)+market_regime表
- market_watch.py: 每30分钟调度时同步更新 market_regime
- strategy_lifecycle.py: enrich_timing_signal 新增市场阶段因子+降级逻辑(震荡/下跌市买入信号降级为关注), regenerate_all/reassess_with_context 加载regime
2026-08-02 16:29:45 +08:00

260 lines
9.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""market_regime.py — 大盘市场阶段判断(实盘版)
解决实盘"无法判断当前是什么市"的断层:
回测用 _load_index_ctx('sh000001') 算 above_ma20/ma20_slope/roc/adx 做市场过滤
v_next4 要求 above_ma20=True + adx>=20 才出手,v_mr 用 mkt_mode='any'),
但实盘 strategy_lifecycle 只有当日涨跌幅 mood(±1%/1.5%→仓位乘数 0.8~1.1),
没有 MA20+ADX 的市场阶段判断 —— 本模块把回测算法原样搬到实盘。
算法与 /home/hmo/MoFin/strategy_lab.py 的 _load_index_ctx 完全一致:
- above_ma20 : 收盘价 > MA20
- ma20_slope : MA20 相对 5 日前 MA20 的变化率(%)
- roc : 10 日 Rate of Change(%)
- adx : 简易趋势强度 (DI+ - DI-)/(DI+ + DI-) 归一化,14 日窗口
calc_trend_strengthbacktest_framework.py 87 行)
regime 分类(与回测 v_next4/v_mr 分工对齐):
- trend_up : above_ma20=True 且 adx>=20 → 趋势市,v_next4 主战场(追涨有效)
- choppy : adx<20 → 震荡市,v_mr 主战场(超跌反弹)
- trend_down : above_ma20=False 且 adx>=20 → 下跌趋势,v_mr 主战场(深超跌)
用法:
python3 market_regime.py # 计算并写 market_regime 表(供定时调度)
python3 market_regime.py --print # 只打印当前市场状态
作为库: from market_regime import compute_regime, load_market_regime
写表: market_regime(date PK, above_ma20, ma20_slope, roc, adx, regime, close, created_at)
数据源: mofin.db stock_daily 的 sh000001import_full_stocks 每日收盘后更新,
盘中用最新可得日线,未收盘日不计入最终判断,adx 用真实历史)。
"""
import sys
import json
import sqlite3
from pathlib import Path
from datetime import datetime
# ── 路径注入:可被 deploy/profile-scripts 下脚本直接 import ──
_SCRIPT_DIR = Path(__file__).resolve().parent
_MOFIN_ROOT = _SCRIPT_DIR.parent.parent # deploy/profile-scripts → MoFin
for _p in (str(_SCRIPT_DIR), str(_MOFIN_ROOT)):
if _p not in sys.path:
sys.path.insert(0, _p)
DB_PATH = Path(_MOFIN_ROOT) / "data" / "mofin.db"
INDEX_CODE = "sh000001" # 上证指数(A股)
INDEX_CODE_HK = "hkHSI" # 恒生指数(港股)
# ADX 阈值与回测 v_next4 的 mkt_adx_min=20 对齐
ADX_TREND_MIN = 20.0
def calc_trend_strength(highs, lows, closes, n=14):
"""简易趋势强度(替代 ADX: (DI+ - DI-) / (DI+ + DI-) 归一化
与 backtest_framework.calc_trend_strength 完全同算法(内联,避免跨目录依赖)"""
tr = []
for i in range(len(highs)):
if i == 0:
tr.append(highs[i] - lows[i])
else:
tr.append(max(highs[i] - lows[i],
abs(highs[i] - closes[i - 1]),
abs(lows[i] - closes[i - 1])))
up = [highs[i] - highs[i - 1] for i in range(1, len(highs))]
down = [lows[i - 1] - lows[i] for i in range(1, len(lows))]
di_plus_raw = [0.0] * len(up)
di_minus_raw = [0.0] * len(up)
for i in range(len(up)):
if up[i] > down[i] and up[i] > 0:
di_plus_raw[i] = up[i]
if down[i] > up[i] and down[i] > 0:
di_minus_raw[i] = down[i]
tr_val = tr[i + 1] if (i + 1) < len(tr) else tr[-1]
if tr_val > 0:
di_plus_raw[i] = di_plus_raw[i] / tr_val * 100
di_minus_raw[i] = di_minus_raw[i] / tr_val * 100
result = []
for i in range(len(di_plus_raw)):
if i < n - 1:
result.append(None)
else:
avg_plus = sum(di_plus_raw[i - n + 1:i + 1]) / n
avg_minus = sum(di_minus_raw[i - n + 1:i + 1]) / n
if avg_plus + avg_minus > 0:
dx = abs(avg_plus - avg_minus) / (avg_plus + avg_minus) * 100
else:
dx = 0
result.append(dx)
return [None] * (len(highs) - len(result)) + result
def calc_ma(series, n):
result = []
for i in range(len(series)):
if i < n - 1:
result.append(None)
else:
result.append(sum(series[i - n + 1:i + 1]) / n)
return result
def calc_roc(series, n=10):
result = []
for i in range(len(series)):
if i < n:
result.append(None)
else:
result.append((series[i] - series[i - n]) / series[i - n] * 100 if series[i - n] != 0 else 0)
return result
def compute_regime(index_code=INDEX_CODE, db_path=None, lookback_days=120):
"""计算大盘市场状态(最近一个完整交易日)。
返回 dict
{date, close, above_ma20, ma20_slope, roc, adx, regime, computed_at}
数据不足(<30行)或取数失败时返回 None。
"""
db = db_path or DB_PATH
conn = sqlite3.connect(str(db), timeout=5)
try:
rows = conn.execute(
"SELECT date, close, high, low FROM stock_daily "
"WHERE code=? ORDER BY date DESC LIMIT ?",
(index_code, lookback_days)).fetchall()
finally:
conn.close()
if not rows or len(rows) < 30:
return None
# 升序处理(与回测 prepare_bars 一致)
rows = list(reversed(rows))
dates = [r[0] for r in rows]
closes = [r[1] for r in rows]
highs = [r[2] for r in rows]
lows = [r[3] for r in rows]
ma20 = calc_ma(closes, 20)
trend = calc_trend_strength(highs, lows, closes)
roc = calc_roc(closes)
i = len(closes) - 1 # 最新一日
close = closes[i]
m20 = ma20[i]
above_ma20 = (close > m20) if m20 else None
# ma20_slope: 相对 5 日前 MA20 的变化率
slope = None
if i >= 5 and ma20[i - 5] and ma20[i - 5] > 0 and m20:
slope = round((m20 - ma20[i - 5]) / ma20[i - 5] * 100, 3)
adx = trend[i] if i < len(trend) else None
roc_v = roc[i] if i < len(roc) else None
# regime 分类
if above_ma20 is True and adx is not None and adx >= ADX_TREND_MIN:
regime = "trend_up"
elif adx is not None and adx < ADX_TREND_MIN:
regime = "choppy"
else:
regime = "trend_down"
return {
"date": dates[i],
"close": close,
"above_ma20": above_ma20,
"ma20_slope": slope,
"roc": round(roc_v, 3) if roc_v is not None else None,
"adx": round(adx, 2) if adx is not None else None,
"regime": regime,
"computed_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
}
def save_regime(regime, db_path=None):
"""写入 market_regime 表(按 date 去重,同日覆盖)"""
if not regime:
return False
db = db_path or DB_PATH
conn = sqlite3.connect(str(db), timeout=5)
try:
conn.execute("""
CREATE TABLE IF NOT EXISTS market_regime (
date TEXT PRIMARY KEY,
above_ma20 INTEGER,
ma20_slope REAL,
roc REAL,
adx REAL,
regime TEXT,
close REAL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
conn.execute("""
INSERT OR REPLACE INTO market_regime
(date, above_ma20, ma20_slope, roc, adx, regime, close, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
""", (
regime["date"],
1 if regime["above_ma20"] else 0,
regime.get("ma20_slope"),
regime.get("roc"),
regime.get("adx"),
regime["regime"],
regime.get("close"),
))
conn.commit()
finally:
conn.close()
return True
def load_market_regime(db_path=None):
"""读取最新 market_regime(供 strategy_lifecycle 等消费方调用)"""
db = db_path or DB_PATH
conn = sqlite3.connect(str(db), timeout=5)
try:
row = conn.execute(
"SELECT date, above_ma20, ma20_slope, roc, adx, regime, close "
"FROM market_regime ORDER BY date DESC LIMIT 1").fetchone()
finally:
conn.close()
if not row:
return None
return {
"date": row[0],
"above_ma20": bool(row[1]),
"ma20_slope": row[2],
"roc": row[3],
"adx": row[4],
"regime": row[5],
"close": row[6],
}
REGIME_DESC = {
"trend_up": "趋势市(大盘MA20上方+ADX≥20):v_next4 追涨主战场",
"choppy": "震荡市(ADX<20):v_mr 超跌反弹主战场",
"trend_down": "下跌趋势(大盘MA20下方+ADX≥20):v_mr 深超跌主战场",
}
def main():
regime = compute_regime()
if "--print" in sys.argv or not regime:
if not regime:
print("sh000001 数据不足,无法计算市场状态")
return 1
print(json.dumps(regime, ensure_ascii=False, indent=2))
print(f"判断: {REGIME_DESC.get(regime['regime'], '')}")
return 0
ok = save_regime(regime)
if ok:
print(f"[market_regime] {regime['date']}{regime['regime']} "
f"(above_ma20={regime['above_ma20']} adx={regime['adx']} "
f"slope={regime['ma20_slope']} roc={regime['roc']})")
else:
print("[market_regime] 写入失败")
return 1
return 0
if __name__ == "__main__":
sys.exit(main())