feat: 测温机制定稿——三态骨架+温度(rsi)连续维度(数据验证) 回填market_regime 10年(2016-2026 2319条); 温度分档temp_band(panic/fear/neutral/greed/euphoria); strategy_router v2三态选策略+温度乘数; 废弃四态thermometer; 策略-适用温度表(v_weak=choppy×fear, v_oversold=trend_down×panic, v_next4=trend_up 63%)

This commit is contained in:
xxm
2026-08-13 09:13:04 +08:00
parent 9310149171
commit 06fbdf5961
4 changed files with 286 additions and 232 deletions
@@ -1,176 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""market_thermometer.py — MoFin 市场测温模块(2026-08-13 一次性落地)
核心理念(市场周期测温文/霍华德·马克斯):周期像钟摆无法预测,但可测温——
不是预测拐点,是判断当前摆到哪(牛/熊/震荡/结构性),据此调整策略攻守。
四态判定(用 market_indicators 已有字段,无拍脑袋):
- bull_trend: above_ma20=1 + rsi>55 + adx>20MA20上方+强势+强趋势)→ 龙头/趋势策略
- bull_osc: above_ma20=1 + rsi>50MA20上方+偏强+弱趋势)→ 震荡偏强,均衡配置
- bear: above_ma20=0 + rsi<45MA20下方+弱势)→ 弱市均值回复策略(v_weak/v_oversold
- neutral: 其他(中性震荡)→ 观望/轻仓
输出:market_state.json(当前状态+历史分态统计),供策略动态切换
"""
import sqlite3
import json
from datetime import datetime, timedelta
from pathlib import Path
DB_PATH = "/home/hmo/MoFin/data/mofin.db"
OUT_PATH = "/home/hmo/MoFin/data/market_state.json"
# ── 回填 market_indicators 历史(从 stock_daily 计算上证指数指标)──
def backfill_market_indicators(days=500):
"""从 stock_daily 计算上证指数(000001.SH)的 mkt_* 指标,回填到 market_indicators"""
c = sqlite3.connect(DB_PATH)
# 上证指数代码(MoFin 约定:000001 = 上证指数)
rows = c.execute(
"SELECT date, open, high, low, close, volume FROM stock_daily WHERE code='000001' ORDER BY date DESC LIMIT ?",
(days,)
).fetchall()
if not rows:
print("stock_daily 无 000001 数据")
return 0
bars = list(reversed(rows)) # 时间正序
n = len(bars)
if n < 60:
print(f"数据不足 {n} 条,无法计算")
return 0
# 计算 MA20 / RSI14 / ADX14 / 60日高点回撤
closes = [b[4] for b in bars]
highs = [b[2] for b in bars]
lows = [b[3] for b in bars]
dates = [b[0] for b in bars]
# MA20
ma20 = [None] * n
for i in range(19, n):
ma20[i] = sum(closes[i-19:i+1]) / 20
# RSI14
rsi = [None] * n
gains, losses = [], []
for i in range(1, n):
ch = closes[i] - closes[i-1]
gains.append(max(ch, 0))
losses.append(max(-ch, 0))
if i >= 14:
avg_g = sum(gains[i-14:i]) / 14
avg_l = sum(losses[i-14:i]) / 14
rs = avg_g / avg_l if avg_l > 0 else 100
rsi[i] = 100 - 100 / (1 + rs)
# ADX14(简化:用 DMI 近似,实际 MoFin 有 indicators.calc_adx,这里用简化版)
adx = [None] * n
tr_list = [0.0]
pdm, ndm = [0.0], [0.0]
for i in range(1, n):
h, l, pc = highs[i], lows[i], closes[i-1]
tr = max(h - l, abs(h - pc), abs(l - pc))
tr_list.append(tr)
up_move = highs[i] - highs[i-1]
down_move = lows[i-1] - lows[i]
pdm.append(up_move if up_move > down_move and up_move > 0 else 0)
ndm.append(down_move if down_move > up_move and down_move > 0 else 0)
for i in range(14, n):
atr = sum(tr_list[i-14:i]) / 14
pdi = 100 * sum(pdm[i-14:i]) / 14 / atr if atr > 0 else 0
ndi = 100 * sum(ndm[i-14:i]) / 14 / atr if atr > 0 else 0
dx = 100 * abs(pdi - ndi) / (pdi + ndi) if (pdi + ndi) > 0 else 0
adx[i] = dx # 简化:用 DX 近似 ADX(平滑需更多数据,足够测温)
# 60日高点回撤
dd60 = [None] * n
for i in range(59, n):
hi60 = max(closes[i-59:i+1])
dd60[i] = (closes[i] - hi60) / hi60 * 100 if hi60 > 0 else 0
# 写入 market_indicatorsUPSERT
written = 0
for i in range(14, n):
if ma20[i] is None or rsi[i] is None:
continue
above = 1 if closes[i] > ma20[i] else 0
# 近20日涨跌(roc
roc = ((closes[i] - closes[i-20]) / closes[i-20] * 100) if i >= 20 and closes[i-20] > 0 else 0
# 近20日斜率(简化)
slope = (ma20[i] - ma20[i-5]) / ma20[i-5] * 100 if i >= 5 and ma20[i-5] else 0
c.execute(
"""INSERT OR REPLACE INTO market_indicators
(date, mkt_rsi, mkt_dd60, mkt_adx, mkt_above_ma20, mkt_down_days, mkt_slope, mkt_roc, updated_at)
VALUES (?,?,?,?,?,?,?,?,?)""",
(dates[i], rsi[i], dd60[i], adx[i], above, 0, slope, roc, datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
)
written += 1
c.commit()
c.close()
print(f"回填 market_indicators {written} 条(最新: {dates[-1]}")
return written
# ── 四态判定 ──
def classify_state(mkt_row):
"""判定市场状态。输入:market_indicators 行 dict"""
rsi = mkt_row.get("mkt_rsi", 50)
adx = mkt_row.get("mkt_adx", 20)
above = mkt_row.get("mkt_above_ma20", 0)
dd60 = mkt_row.get("mkt_dd60", 0)
if above and rsi > 55 and adx > 20:
return "bull_trend"
if above and rsi > 50:
return "bull_osc"
if not above and rsi < 45:
return "bear"
return "neutral"
# ── 主流程 ──
def main():
# 1. 回填历史(500 日 ≈ 2 年)
backfill_market_indicators(500)
# 2. 当前状态
c = sqlite3.connect(DB_PATH)
row = c.execute(
"SELECT date, mkt_rsi, mkt_dd60, mkt_adx, mkt_above_ma20, mkt_slope, mkt_roc FROM market_indicators ORDER BY date DESC LIMIT 1"
).fetchone()
c.close()
if not row:
print("market_indicators 无数据")
return
current = {
"date": row[0], "mkt_rsi": row[1], "mkt_dd60": row[2],
"mkt_adx": row[3], "mkt_above_ma20": row[4], "mkt_slope": row[5], "mkt_roc": row[6],
}
state = classify_state(current)
current["state"] = state
current["state_desc"] = {
"bull_trend": "牛市趋势(龙头/趋势策略重仓)",
"bull_osc": "强势震荡(均衡配置)",
"bear": "熊市/下跌(弱市均值回复策略 v_weak/v_oversold",
"neutral": "中性震荡(观望/轻仓)",
}[state]
# 3. 历史分态统计(近 250 交易日 ≈ 1 年)
c = sqlite3.connect(DB_PATH)
rows = c.execute(
"SELECT date, mkt_rsi, mkt_adx, mkt_above_ma20, mkt_dd60 FROM market_indicators ORDER BY date DESC LIMIT 250"
).fetchall()
c.close()
from collections import Counter
hist = Counter(classify_state({"mkt_rsi": r[1], "mkt_adx": r[2], "mkt_above_ma20": r[3], "mkt_dd60": r[4]}) for r in rows)
current["hist_1y"] = dict(hist)
current["updated_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# 4. 写入 market_state.json
Path(OUT_PATH).write_text(json.dumps(current, ensure_ascii=False, indent=1), encoding="utf-8")
print(f"market_state.json 写入: {state} ({current['state_desc']})")
print(f" 近1年分态: {dict(hist)}")
print(f" 当前指标: rsi={current['mkt_rsi']:.1f} adx={current['mkt_adx']:.1f} above={current['mkt_above_ma20']} dd60={current['mkt_dd60']:.1f}")
if __name__ == "__main__":
main()
+119 -56
View File
@@ -1,74 +1,137 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""strategy_router.py — MoFin 策略动态路由(2026-08-13 一次性落地
"""strategy_router.py v2 — MoFin 策略动态路由(三态 + 温度维度,2026-08-13 重写
核心:读 market_state.json 的当前市场状态,决定各策略权重/开关
- bear: v_weak/v_oversold 降权 50%(信号质量差,28%胜率
- bull_osc: v_weak/v_oversold 正常(60%胜率,当前状态
- bull_trend: v_weak/v_oversold 降权 50%,启用龙头策略(待研究)
- neutral: 轻仓观望
结论(数据验证):三态骨架 + 温度(rsi)连续维度。
- 三态(trend_up/choppy/trend_down)→ 选策略类型(market_regime.py 判定
- 温度(rsi 档位)→ 仓位乘数(temp_band.py 判定,连续量
输出:strategy_weights.json 供扫描器/重评脚本读取
策略-适用温度映射(数据来源:2026-08-13 分市场状态检验,见 docs/decisions/2026-08-13-事项五):
v_weak → choppy × [fear, neutral] (震荡市+恐慌最佳 71%
v_oversold → trend_down × [panic, fear] (下跌市+恐慌最佳 64-86%
v_next4 → trend_up × [greed, euphoria](趋势市 63%
v8.1 → trend_up (趋势市 57%
s2_panic → trend_down × [panic] (极端恐慌 86%
v_lurk_v3 → [choppy, trend_down] × [fear, neutral]77%/71%
输出:strategy_weights.json(各策略 weight 由 状态×温度 决定)
"""
import json
import sys
from pathlib import Path
from datetime import datetime
MARKET_STATE = Path("/home/hmo/MoFin/data/market_state.json")
_SCRIPT_DIR = Path(__file__).resolve().parent
sys.path.insert(0, str(_SCRIPT_DIR))
sys.path.insert(0, "/home/hmo/MoFin")
OUT = Path("/home/hmo/MoFin/data/strategy_weights.json")
def load_market_state():
if MARKET_STATE.exists():
return json.loads(MARKET_STATE.read_text(encoding="utf-8"))
return {"state": "neutral", "date": ""}
def route(state):
"""市场状态 → 策略权重。返回 dict"""
# 基准权重(等权,程飞:不确定时等权最稳健)
base = {"v_weak": 1.0, "v_oversold": 1.0, "leader": 0.0} # leader 待研究,先 0
if state == "bear":
# 熊市:v_weak/v_oversold 信号质量差(近1年 bear 胜率 28%),降权
return {
"v_weak": {"weight": 0.5, "action": "降权50%", "reason": "bear 胜率28%,信号质量差"},
"v_oversold": {"weight": 0.5, "action": "降权50%", "reason": "bear 胜率28%"},
"leader": {"weight": 0.0, "action": "停用", "reason": "bear 不适用龙头"},
"state": state,
def load_regime():
"""读取最新 market_regime(三态)"""
try:
from market_regime import load_market_regime
return load_market_regime()
except Exception:
return None
def get_temp():
"""读取市场温度(rsi 档位)"""
try:
from temp_band import get_market_temp, temp_multiplier
return get_market_temp()
except Exception as e:
print(f" [router] 温度获取失败: {e}", file=sys.stderr)
return {"band": "unknown", "rsi": None}
# 策略-适用温度映射(基础数据,数据驱动固化)
# weight = 状态匹配度 × 温度乘数
STRATEGY_TEMP = {
"v_weak": {
"family": "mr",
"regimes": ["choppy"],
"desc": "震荡市超跌反弹(choppy+恐慌71%最佳)",
},
"v_oversold": {
"family": "mr",
"regimes": ["trend_down"],
"desc": "下跌市超跌(trend_down+恐慌64-86%",
},
"v_next4": {
"family": "trend",
"regimes": ["trend_up"],
"desc": "趋势市追涨(trend_up 63%",
},
"v8.1": {
"family": "trend",
"regimes": ["trend_up"],
"desc": "波段先出再进(trend_up 57%",
},
"s2_panic": {
"family": "mr",
"regimes": ["trend_down"],
"desc": "极端恐慌买超跌(panic 86%",
},
"v_lurk_v3": {
"family": "mr",
"regimes": ["choppy", "trend_down"],
"desc": "潜伏型(choppy/trend_down 71-77%",
},
}
def route(regime, temp):
"""三态 + 温度 → 各策略权重"""
current_regime = regime.get("regime", "unknown") if regime else "unknown"
band = temp.get("band", "unknown")
family = temp.get("family", "mr")
weights = {}
for name, cfg in STRATEGY_TEMP.items():
# 状态匹配度:适用温区包含当前状态 → 1.0,否则 0.3(保留观察)
matched = current_regime in cfg["regimes"]
base = 1.0 if matched else 0.3
# 温度乘数(按策略家族)
try:
from temp_band import temp_multiplier
mult = temp_multiplier(band, cfg["family"])
except Exception:
mult = 0.8
weights[name] = {
"weight": round(base * mult, 2),
"regime": current_regime,
"temp_band": band,
"family": cfg["family"],
"matched": matched,
"desc": cfg["desc"],
}
if state == "bull_osc":
# 强势震荡:v_weak/v_oversold 最有效(60%胜率),正常
return {
"v_weak": {"weight": 1.0, "action": "正常", "reason": "bull_osc 胜率60%,最有效"},
"v_oversold": {"weight": 1.0, "action": "正常", "reason": "bull_osc 适用"},
"leader": {"weight": 0.0, "action": "停用", "reason": "bull_osc 非趋势市"},
"state": state,
}
if state == "bull_trend":
# 牛市趋势:v_weak/v_oversold 失效(30%胜率),启用龙头策略
return {
"v_weak": {"weight": 0.5, "action": "降权50%", "reason": "bull_trend 胜率30%"},
"v_oversold": {"weight": 0.5, "action": "降权50%", "reason": "bull_trend 失效"},
"leader": {"weight": 1.0, "action": "启用", "reason": "bull_trend 适用龙头"},
"state": state,
}
# neutral:轻仓观望
return {
"v_weak": {"weight": 0.5, "action": "轻仓", "reason": "neutral 观望"},
"v_oversold": {"weight": 0.5, "action": "轻仓", "reason": "neutral 观望"},
"leader": {"weight": 0.0, "action": "停用", "reason": "neutral 观望"},
"state": state,
}
return weights
def main():
ms = load_market_state()
state = ms.get("state", "neutral")
weights = route(state)
weights["market_state"] = ms
weights["updated_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
OUT.write_text(json.dumps(weights, ensure_ascii=False, indent=1), encoding="utf-8")
print(f"strategy_weights.json 写入: {state}")
for k, v in weights.items():
if isinstance(v, dict) and "weight" in v:
print(f" {k}: weight={v['weight']} ({v['action']}) - {v['reason']}")
regime = load_regime()
temp = get_temp()
if not regime:
print(" [router] market_regime 无数据", file=sys.stderr)
regime = {"regime": "unknown"}
weights = route(regime, temp)
out = {
"state": regime.get("regime", "unknown"),
"temp_band": temp.get("band", "unknown"),
"temp_rsi": temp.get("rsi"),
"weights": weights,
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"note": "三态(trend_up/choppy/trend_down)选策略类型 + 温度(rsi)乘数决定仓位",
}
OUT.write_text(json.dumps(out, ensure_ascii=False, indent=1), encoding="utf-8")
print(f"strategy_weights.json: 状态={out['state']} 温度={out['temp_band']}(rsi={out['temp_rsi']})")
for name, w in weights.items():
print(f" {name:<12} weight={w['weight']:<5} regime={w['regime']:<10} band={w['temp_band']:<8} {'' if w['matched'] else '观察'}")
if __name__ == "__main__":
main()
+111
View File
@@ -0,0 +1,111 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""temp_band.py — 市场温度分档模块(2026-08-13)
结论(数据验证):三态骨架 + 温度(rsi)连续维度。
- 三态(trend_up/choppy/trend_down= market_regime.py 已有(策略类型分类)
- 温度(rsi 分档)= 本模块(力度维度,仓位乘数)
数据依据(2026-08-13 分市场状态检验):
v_weak choppy: 恐慌<45 -> 71%胜率 / 中性45-60 -> 46% / 亢奋>60 -> 37%
v_oversold trend_down: 恐慌<35 -> 64% / 偏弱35-50 -> 53% / 中性>50 -> 0%
s2_panic trend_down 恐慌<35: 86%胜率 +16.6%
档位:panic(<35) / fear(35-45) / neutral(45-60) / greed(60-70) / euphoria(>70)
用法:
from temp_band import get_market_temp, temp_multiplier
t = get_market_temp() # {rsi, band, regime}
mult = temp_multiplier(t["band"], strategy_family="mr")
"""
import sqlite3
from pathlib import Path
DB = Path("/home/hmo/MoFin/data/mofin.db")
INDEX = "sh000001"
def calc_rsi(series, n=14):
result = [None] * len(series)
if len(series) < n + 1:
return result
gains, losses = [], []
for i in range(1, len(series)):
ch = series[i] - series[i - 1]
gains.append(max(ch, 0))
losses.append(max(-ch, 0))
if i >= n:
avg_g = sum(gains[i - n:i]) / n
avg_l = sum(losses[i - n:i]) / n
rs = avg_g / avg_l if avg_l > 0 else 100
result[i] = 100 - 100 / (1 + rs)
return result
def temp_band(rsi):
"""连续 rsi -> 温度档位"""
if rsi is None:
return "unknown"
if rsi < 35:
return "panic"
if rsi < 45:
return "fear"
if rsi < 60:
return "neutral"
if rsi < 70:
return "greed"
return "euphoria"
def temp_multiplier(band, strategy_family="mr"):
"""温度 -> 仓位乘数。mr=均值回复(恐慌重仓),trend=趋势(恐慌回避)"""
if strategy_family == "trend":
return {
"panic": 0.0, "fear": 0.0, "neutral": 0.5,
"greed": 1.0, "euphoria": 0.5, "unknown": 0.5,
}.get(band, 0.5)
return {
"panic": 1.5, "fear": 1.0, "neutral": 0.8,
"greed": 0.5, "euphoria": 0.3, "unknown": 0.5,
}.get(band, 0.5)
def get_market_temp(db_path=None):
"""读取最新市场温度(rsi + 档位 + regime)。"""
db = db_path or DB
conn = sqlite3.connect(str(db), timeout=5)
try:
# 上证最近 30 日收盘算 rsi
rows = conn.execute(
"SELECT date, close FROM stock_daily WHERE code=? ORDER BY date DESC LIMIT 30",
(INDEX,)
).fetchall()
# 最新 regime
reg = conn.execute(
"SELECT date, regime FROM market_regime ORDER BY date DESC LIMIT 1"
).fetchone()
finally:
conn.close()
if not rows or len(rows) < 15:
return {"rsi": None, "band": "unknown", "regime": "unknown", "date": ""}
rows = list(reversed(rows))
closes = [r[1] for r in rows]
dates = [r[0] for r in rows]
rsi_series = calc_rsi(closes)
rsi_now = rsi_series[-1] if rsi_series else None
band = temp_band(rsi_now)
regime = reg[1] if reg else "unknown"
reg_date = reg[0] if reg else ""
return {
"rsi": round(rsi_now, 1) if rsi_now is not None else None,
"band": band,
"regime": regime,
"date": dates[-1],
"regime_date": reg_date,
}
if __name__ == "__main__":
t = get_market_temp()
print(f"温度: rsi={t['rsi']} band={t['band']} regime={t['regime']} ({t['date']})")
print(f" mr 乘数: {temp_multiplier(t['band'], 'mr')}")
print(f" trend 乘数: {temp_multiplier(t['band'], 'trend')}")
@@ -149,3 +149,59 @@
---
**结论**:近1年不佳不是策略坏了,是市场在 bear 状态时间太长(111/250 天)vs 弱市策略类型错配。解法 = 市场测温动态切换(bear 降权/bull_osc 正常/bull_trend 换龙头)+ 新增龙头策略(bull_trend 武器)+ 失效预警淘汰(三振出局)。
---
## 六、测温机制定稿:三态骨架 + 温度(rsi)连续维度(2026-08-13 数据验证后修正)
### 6.1 三态 vs 四态对比(老莫提问,数据决定)
**对比实验**:用 market_regime 回填 10 年(2016-2026,2319 条),按三态分组所有历史策略交易:
| 策略 | trend_up | choppy | trend_down | 适用温区 |
|---|---|---|---|---|
| v_weak | 46% | **55%** | 47% | **choppy** |
| v_oversold | 50% | 57% | **60%/+12.5%** | **trend_down** |
| v_next4 | **63%/+13.6%** | - | - | **trend_up** |
| v8.1 | **57%/+9.4%** | - | - | **trend_up** |
| s2_panic | - | - | **86%/+16.6%** | **trend_down×恐慌** |
| v_lurk_v3 | - | 77% | 71% | choppy/trend_down |
| v_lurk_bull | 33%/-4.7% | 35%/-4.0% | - | 全失效(负收益)|
**choppy 内部按 rsi 温度分组(决定性验证)**
| rsi 档 | v_weak 胜率 | 均盈 |
|---|---|---|
| 恐慌<45 | **71%** | +6.43% |
| 中性45-60 | 46% | +1.74% |
| 亢奋>60 | 37% | -0.16% |
### 6.2 结论:三态够用,温度是叠加的连续维度
1. **三态(trend_up/choppy/trend_down= 策略类型分类**(用哪类策略)——market_regime.py 已有,回填 10 年完成
2. **温度(rsi 分档)= 力度维度**(仓位乘数,连续量)——不并入状态分类(四态会把 rsi 硬切成 50/55 拍脑袋阈值,丢失连续性斜坡:恐慌71%→中性46%→亢奋37%)
3. **废弃 market_thermometer 四态**(避免重复基建,数据纪律:统一 market_regime 三态)
### 6.3 策略-适用温度表(数据驱动固化,STRATEGY_TEMP 基础数据)
| 策略 | 适用状态 | 温度档 | 数据依据 |
|---|---|---|---|
| v_weak | choppy | fear/neutral | 恐慌71% |
| v_oversold | trend_down | panic/fear | 64-86% |
| v_next4 | trend_up | greed/euphoria | 63% |
| v8.1 | trend_up | - | 57% |
| s2_panic | trend_down | panic | 86% |
| v_lurk_v3 | choppy/trend_down | fear/neutral | 71-77% |
**关键发现**:趋势市策略 v_next4/v8.1 已躺在历史列表(不需要新发明 leader_scanner),三态×已有策略 = 完整武器库。
### 6.4 落地文件
| 文件 | 功能 |
|---|---|
| market_regime.py | 三态判定(已有,回填10年) |
| temp_band.py | 温度分档 + 仓位乘数(panic/fear/neutral/greed/euphoria|
| strategy_router.py v2 | 三态选策略 + 温度乘数 → strategy_weights.json |
| strategy_alert.py | 三振出局失效预警 |
当前:choppy×neutral → v_weak/v_lurk_v3 weight=0.8v_next4/v8.1 观察。