feat: 温区双市场改造——market_regime/temp_band/regime_tracker/regime_perf支持market维度(A股默认行为不变,港股hkHSI独立温区)+migrate_regime_market迁移脚本(4表加market列,幂等+备份)+backfill_hk_index恒指历史回填脚本
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""backfill_hk_index.py — 恒生指数(hkHSI)历史日K回填(一次性脚本)
|
||||
|
||||
背景(阶段3 港股接入):stock_daily 的 hkHSI 仅 2024 起(643 条),温区回填
|
||||
需要更长历史(对齐 A 股约 10 年)。本脚本从腾讯日K接口拉取 hkHSI 全量历史
|
||||
(约 3000 根 ≈ 12 年),写入 stock_daily。
|
||||
|
||||
数据源(与 market_data.fetch_tx_klines 同一腾讯接口,hkHSI 已验证返回 day 键):
|
||||
http://ifzq.gtimg.cn/appstock/app/fqkline/get?param=hkHSI,day,,,{count},qfq
|
||||
|
||||
用法:
|
||||
python3 backfill_hk_index.py # 拉 3000 根(约12年)
|
||||
python3 backfill_hk_index.py --count 4000
|
||||
|
||||
写入:INSERT OR IGNORE((code,date) 唯一键防重复,已有日期跳过)。
|
||||
"""
|
||||
import json
|
||||
import sqlite3
|
||||
import sys
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
_SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
_MOFIN_ROOT = _SCRIPT_DIR.parent.parent
|
||||
DB_PATH = Path(_MOFIN_ROOT) / "data" / "mofin.db"
|
||||
|
||||
UA = "Mozilla/5.0"
|
||||
INDEX_CODE = "hkHSI"
|
||||
|
||||
|
||||
def fetch_hsi(count=3000):
|
||||
"""从腾讯接口拉 hkHSI 日K(count 根)"""
|
||||
url = (f"http://ifzq.gtimg.cn/appstock/app/fqkline/get?"
|
||||
f"param={INDEX_CODE},day,,,{count},qfq")
|
||||
req = urllib.request.Request(url, headers={"User-Agent": UA})
|
||||
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
|
||||
with opener.open(req, timeout=30) as r:
|
||||
text = r.read().decode("utf-8", errors="replace")
|
||||
data = json.loads(text)
|
||||
node = data.get("data", {}).get(INDEX_CODE, {})
|
||||
bars = node.get("qfqday") or node.get("day") or []
|
||||
return bars
|
||||
|
||||
|
||||
def main():
|
||||
count = 3000
|
||||
for a in sys.argv[1:]:
|
||||
if a.startswith("--count"):
|
||||
count = int(a.split("=")[-1] if "=" in a else sys.argv[sys.argv.index(a) + 1])
|
||||
|
||||
print(f"拉取 {INDEX_CODE} 最近 {count} 根日K...")
|
||||
bars = fetch_hsi(count)
|
||||
if not bars:
|
||||
print("拉取失败/为空,终止")
|
||||
return 1
|
||||
print(f"拉到 {len(bars)} 根,时间范围 {bars[0][0]} ~ {bars[-1][0]}")
|
||||
|
||||
conn = sqlite3.connect(str(DB_PATH), timeout=30)
|
||||
conn.execute("PRAGMA busy_timeout=30000")
|
||||
# stock_daily 列:code, date, open, close, high, low, volume(与 daily_kline_collector 一致)
|
||||
inserted = 0
|
||||
skipped = 0
|
||||
for b in bars:
|
||||
# 格式: [date, open, close, high, low, volume, ...]
|
||||
try:
|
||||
date, open_, close, high, low = b[0], b[1], b[2], b[3], b[4]
|
||||
volume = float(b[5]) if len(b) > 5 and b[5] not in (None, "") else 0
|
||||
except (IndexError, ValueError):
|
||||
skipped += 1
|
||||
continue
|
||||
cur = conn.execute(
|
||||
"INSERT OR IGNORE INTO stock_daily (code, date, open, close, high, low, volume) "
|
||||
"VALUES (?,?,?,?,?,?,?)",
|
||||
(INDEX_CODE, date, float(open_), float(close), float(high), float(low), volume)
|
||||
)
|
||||
inserted += cur.rowcount
|
||||
conn.commit()
|
||||
# 验证
|
||||
total = conn.execute(
|
||||
"SELECT COUNT(*), MIN(date), MAX(date) FROM stock_daily WHERE code=?", (INDEX_CODE,)
|
||||
).fetchone()
|
||||
conn.close()
|
||||
print(f"写入 {inserted} 根(跳过 {skipped} 根格式异常 / 已有日期自动 IGNORE)")
|
||||
print(f"stock_daily 的 {INDEX_CODE}: 共 {total[0]} 条,{total[1]} ~ {total[2]}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -24,8 +24,9 @@ regime 分类(与回测 v_next4/v_mr 分工对齐):
|
||||
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 的 sh000001(import_full_stocks 每日收盘后更新,
|
||||
写表: market_regime((date, market) PK, above_ma20, ma20_slope, roc, adx, regime, close, created_at)
|
||||
market='a'(A股 sh000001) / 'hk'(港股 hkHSI),同一交易日两个市场各一条(2026-08-14 阶段3 双市场)
|
||||
数据源: mofin.db stock_daily 的 sh000001/hkHSI(每日收盘后更新,
|
||||
盘中用最新可得日线,未收盘日不计入最终判断,adx 用真实历史)。
|
||||
"""
|
||||
import sys
|
||||
@@ -167,8 +168,8 @@ def compute_regime(index_code=INDEX_CODE, db_path=None, lookback_days=120):
|
||||
}
|
||||
|
||||
|
||||
def save_regime(regime, db_path=None):
|
||||
"""写入 market_regime 表(按 date 去重,同日覆盖)"""
|
||||
def save_regime(regime, market='a', db_path=None):
|
||||
"""写入 market_regime 表(按 (date, market) 去重,同日同市场覆盖)"""
|
||||
if not regime:
|
||||
return False
|
||||
db = db_path or DB_PATH
|
||||
@@ -176,22 +177,25 @@ def save_regime(regime, db_path=None):
|
||||
try:
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS market_regime (
|
||||
date TEXT PRIMARY KEY,
|
||||
date TEXT NOT NULL,
|
||||
market TEXT NOT NULL DEFAULT 'a',
|
||||
above_ma20 INTEGER,
|
||||
ma20_slope REAL,
|
||||
roc REAL,
|
||||
adx REAL,
|
||||
regime TEXT,
|
||||
close REAL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (date, market)
|
||||
)
|
||||
""")
|
||||
conn.execute("""
|
||||
INSERT OR REPLACE INTO market_regime
|
||||
(date, above_ma20, ma20_slope, roc, adx, regime, close, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||
(date, market, above_ma20, ma20_slope, roc, adx, regime, close, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||
""", (
|
||||
regime["date"],
|
||||
market,
|
||||
1 if regime["above_ma20"] else 0,
|
||||
regime.get("ma20_slope"),
|
||||
regime.get("roc"),
|
||||
@@ -205,14 +209,16 @@ def save_regime(regime, db_path=None):
|
||||
return True
|
||||
|
||||
|
||||
def load_market_regime(db_path=None):
|
||||
"""读取最新 market_regime(供 strategy_lifecycle 等消费方调用)"""
|
||||
def load_market_regime(db_path=None, market='a'):
|
||||
"""读取指定市场最新 market_regime(供 strategy_lifecycle 等消费方调用)。
|
||||
market='a' 默认 → 与改造前行为完全一致(A 股温区)"""
|
||||
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()
|
||||
"FROM market_regime WHERE market=? ORDER BY date DESC LIMIT 1",
|
||||
(market,)).fetchone()
|
||||
finally:
|
||||
conn.close()
|
||||
if not row:
|
||||
@@ -236,21 +242,33 @@ REGIME_DESC = {
|
||||
|
||||
|
||||
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] 写入失败")
|
||||
# 双市场:A股(sh000001) + 港股(hkHSI),各自独立温区
|
||||
results = {}
|
||||
for market, index_code in (("a", INDEX_CODE), ("hk", INDEX_CODE_HK)):
|
||||
r = compute_regime(index_code=index_code)
|
||||
if not r:
|
||||
print(f"[market_regime] {index_code} 数据不足,跳过 {market} 市场", flush=True)
|
||||
continue
|
||||
results[market] = (index_code, r)
|
||||
|
||||
if "--print" in sys.argv:
|
||||
for market, (idx, regime) in results.items():
|
||||
print(json.dumps(regime, ensure_ascii=False, indent=2))
|
||||
print(f"[{market}] {idx} 判断: {REGIME_DESC.get(regime['regime'], '')}")
|
||||
return 0 if results else 1
|
||||
|
||||
ok_any = False
|
||||
for market, (idx, regime) in results.items():
|
||||
ok = save_regime(regime, market=market)
|
||||
if ok:
|
||||
ok_any = True
|
||||
print(f"[market_regime] {market} {regime['date']} → {regime['regime']} "
|
||||
f"(above_ma20={regime['above_ma20']} adx={regime['adx']} "
|
||||
f"slope={regime['ma20_slope']} roc={regime['roc']})")
|
||||
else:
|
||||
print(f"[market_regime] {market} 写入失败")
|
||||
if not ok_any:
|
||||
print("[market_regime] 两个市场均写入失败/数据不足")
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""migrate_regime_market.py — 温区系统双市场表迁移(一次性脚本)
|
||||
|
||||
背景(阶段3 港股接入):温区相关 4 张表加 market 维度('a' A股 / 'hk' 港股),
|
||||
A 股历史数据统一回填 market='a',与改造前语义完全一致。
|
||||
|
||||
迁移表与方式:
|
||||
- market_regime : 重建。主键 date → (date, market)。旧数据 market='a'
|
||||
- strategy_regime_perf : 重建。主键 (strategy,regime) → (strategy,regime,market)
|
||||
- market_indicators : 重建。主键 date → (date, market)。旧数据 market='a'
|
||||
- regime_cycles : ALTER 加列(主键 id 不变,无需重建)
|
||||
|
||||
幂等:每张表先查 market 列是否已存在,已存在则跳过(可重复执行)。
|
||||
备份:迁移前每张表备份为 xxx_bak_20260814(CREATE TABLE AS SELECT,含数据,
|
||||
不含约束——回滚时数据完整可导回)。数据库另有每日备份。
|
||||
|
||||
用法:
|
||||
python3 migrate_regime_market.py # 迁移全部 4 张表
|
||||
python3 migrate_regime_market.py --check # 只检查现状,不迁移
|
||||
"""
|
||||
import sqlite3
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
_SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
_MOFIN_ROOT = _SCRIPT_DIR.parent.parent
|
||||
DB_PATH = Path(_MOFIN_ROOT) / "data" / "mofin.db"
|
||||
|
||||
BAK_SUFFIX = "_bak_20260814"
|
||||
TABLES = ["market_regime", "regime_cycles", "strategy_regime_perf", "market_indicators"]
|
||||
|
||||
|
||||
def _connect():
|
||||
conn = sqlite3.connect(str(DB_PATH), timeout=30)
|
||||
conn.execute("PRAGMA busy_timeout=30000")
|
||||
return conn
|
||||
|
||||
|
||||
def _table_exists(conn, name):
|
||||
row = conn.execute(
|
||||
"SELECT 1 FROM sqlite_master WHERE type='table' AND name=?", (name,)
|
||||
).fetchone()
|
||||
return row is not None
|
||||
|
||||
|
||||
def _has_column(conn, table, col):
|
||||
cols = [r[1] for r in conn.execute(f"PRAGMA table_info({table})")]
|
||||
return col in cols
|
||||
|
||||
|
||||
def _backup_table(conn, table):
|
||||
"""迁移前备份:CREATE TABLE xxx_bak_20260814 AS SELECT * FROM xxx"""
|
||||
bak = table + BAK_SUFFIX
|
||||
conn.execute(f"DROP TABLE IF EXISTS {bak}")
|
||||
conn.execute(f"CREATE TABLE {bak} AS SELECT * FROM {table}")
|
||||
n = conn.execute(f"SELECT COUNT(*) FROM {bak}").fetchone()[0]
|
||||
print(f" [备份] {table} → {bak} ({n} 行)")
|
||||
return bak
|
||||
|
||||
|
||||
def _migrate_market_regime(conn):
|
||||
"""重建:主键 (date, market),旧数据 market='a'"""
|
||||
if _has_column(conn, "market_regime", "market"):
|
||||
print("[跳过] market_regime 已有 market 列")
|
||||
return
|
||||
_backup_table(conn, "market_regime")
|
||||
conn.execute("DROP TABLE IF EXISTS market_regime_new")
|
||||
conn.execute("""
|
||||
CREATE TABLE market_regime_new (
|
||||
date TEXT NOT NULL,
|
||||
market TEXT NOT NULL DEFAULT 'a',
|
||||
above_ma20 INTEGER,
|
||||
ma20_slope REAL,
|
||||
roc REAL,
|
||||
adx REAL,
|
||||
regime TEXT,
|
||||
close REAL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (date, market)
|
||||
)
|
||||
""")
|
||||
conn.execute("""
|
||||
INSERT INTO market_regime_new
|
||||
(date, market, above_ma20, ma20_slope, roc, adx, regime, close, created_at)
|
||||
SELECT date, 'a', above_ma20, ma20_slope, roc, adx, regime, close, created_at
|
||||
FROM market_regime
|
||||
""")
|
||||
conn.execute("DROP TABLE market_regime")
|
||||
conn.execute("ALTER TABLE market_regime_new RENAME TO market_regime")
|
||||
n = conn.execute("SELECT COUNT(*) FROM market_regime").fetchone()[0]
|
||||
print(f" [完成] market_regime: {n} 行, 主键 (date,market), 历史数据 market='a'")
|
||||
|
||||
|
||||
def _migrate_strategy_regime_perf(conn):
|
||||
"""重建:主键 (strategy, regime, market),旧数据 market='a'"""
|
||||
if _has_column(conn, "strategy_regime_perf", "market"):
|
||||
print("[跳过] strategy_regime_perf 已有 market 列")
|
||||
return
|
||||
_backup_table(conn, "strategy_regime_perf")
|
||||
conn.execute("DROP TABLE IF EXISTS strategy_regime_perf_new")
|
||||
conn.execute("""
|
||||
CREATE TABLE strategy_regime_perf_new (
|
||||
strategy TEXT,
|
||||
market TEXT NOT NULL DEFAULT 'a',
|
||||
regime TEXT,
|
||||
trades INTEGER,
|
||||
win_rate REAL,
|
||||
avg_pnl REAL,
|
||||
avg_hold_days REAL,
|
||||
total_return_pct REAL,
|
||||
cagr_pct REAL,
|
||||
portfolio_max_dd_pct REAL,
|
||||
capital_final REAL,
|
||||
positions_taken INTEGER,
|
||||
sharpe_ratio REAL,
|
||||
profit_factor REAL,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (strategy, regime, market)
|
||||
)
|
||||
""")
|
||||
conn.execute("""
|
||||
INSERT INTO strategy_regime_perf_new
|
||||
(strategy, market, regime, trades, win_rate, avg_pnl, avg_hold_days,
|
||||
total_return_pct, cagr_pct, portfolio_max_dd_pct, capital_final,
|
||||
positions_taken, sharpe_ratio, profit_factor, updated_at)
|
||||
SELECT strategy, 'a', regime, trades, win_rate, avg_pnl, avg_hold_days,
|
||||
total_return_pct, cagr_pct, portfolio_max_dd_pct, capital_final,
|
||||
positions_taken, sharpe_ratio, profit_factor, updated_at
|
||||
FROM strategy_regime_perf
|
||||
""")
|
||||
conn.execute("DROP TABLE strategy_regime_perf")
|
||||
conn.execute("ALTER TABLE strategy_regime_perf_new RENAME TO strategy_regime_perf")
|
||||
n = conn.execute("SELECT COUNT(*) FROM strategy_regime_perf").fetchone()[0]
|
||||
print(f" [完成] strategy_regime_perf: {n} 行, 主键 (strategy,regime,market)")
|
||||
|
||||
|
||||
def _migrate_market_indicators(conn):
|
||||
"""重建:主键 date → (date, market),旧数据 market='a'"""
|
||||
if _has_column(conn, "market_indicators", "market"):
|
||||
print("[跳过] market_indicators 已有 market 列")
|
||||
return
|
||||
_backup_table(conn, "market_indicators")
|
||||
conn.execute("DROP TABLE IF EXISTS market_indicators_new")
|
||||
conn.execute("""
|
||||
CREATE TABLE market_indicators_new (
|
||||
date TEXT NOT NULL,
|
||||
market TEXT NOT NULL DEFAULT 'a',
|
||||
mkt_rsi REAL, mkt_dd60 REAL, mkt_adx REAL, mkt_above_ma20 INTEGER,
|
||||
mkt_down_days INTEGER, mkt_slope REAL, mkt_roc REAL,
|
||||
updated_at TEXT,
|
||||
PRIMARY KEY (date, market)
|
||||
)
|
||||
""")
|
||||
conn.execute("""
|
||||
INSERT INTO market_indicators_new
|
||||
(date, market, mkt_rsi, mkt_dd60, mkt_adx, mkt_above_ma20,
|
||||
mkt_down_days, mkt_slope, mkt_roc, updated_at)
|
||||
SELECT date, 'a', mkt_rsi, mkt_dd60, mkt_adx, mkt_above_ma20,
|
||||
mkt_down_days, mkt_slope, mkt_roc, updated_at
|
||||
FROM market_indicators
|
||||
""")
|
||||
conn.execute("DROP TABLE market_indicators")
|
||||
conn.execute("ALTER TABLE market_indicators_new RENAME TO market_indicators")
|
||||
n = conn.execute("SELECT COUNT(*) FROM market_indicators").fetchone()[0]
|
||||
print(f" [完成] market_indicators: {n} 行, 主键 (date,market)")
|
||||
|
||||
|
||||
def _migrate_regime_cycles(conn):
|
||||
"""ALTER 加列:主键 id 不变,无需重建。market TEXT NOT NULL DEFAULT 'a'"""
|
||||
if _has_column(conn, "regime_cycles", "market"):
|
||||
print("[跳过] regime_cycles 已有 market 列")
|
||||
return
|
||||
_backup_table(conn, "regime_cycles")
|
||||
conn.execute("ALTER TABLE regime_cycles ADD COLUMN market TEXT NOT NULL DEFAULT 'a'")
|
||||
n = conn.execute("SELECT COUNT(*) FROM regime_cycles").fetchone()[0]
|
||||
print(f" [完成] regime_cycles: {n} 行, 已加 market 列 (id 主键保留)")
|
||||
|
||||
|
||||
def check_status(conn):
|
||||
"""检查现状(--check):每张表是否存在、是否已有 market 列"""
|
||||
print(f"DB: {DB_PATH}")
|
||||
for t in TABLES:
|
||||
if not _table_exists(conn, t):
|
||||
print(f" {t:<24} 表不存在")
|
||||
continue
|
||||
n = conn.execute(f"SELECT COUNT(*) FROM {t}").fetchone()[0]
|
||||
m = _has_column(conn, t, "market")
|
||||
cols = ", ".join(r[1] for r in conn.execute(f"PRAGMA table_info({t})"))
|
||||
print(f" {t:<24} {n:>7} 行 | market列: {'有' if m else '缺'} | {cols}")
|
||||
|
||||
|
||||
def main():
|
||||
if not DB_PATH.exists():
|
||||
print(f"DB 不存在: {DB_PATH}")
|
||||
return 1
|
||||
conn = _connect()
|
||||
if "--check" in sys.argv:
|
||||
check_status(conn)
|
||||
conn.close()
|
||||
return 0
|
||||
for t in TABLES:
|
||||
if not _table_exists(conn, t):
|
||||
print(f"[跳过] {t} 表不存在")
|
||||
continue
|
||||
if t == "market_regime":
|
||||
_migrate_market_regime(conn)
|
||||
elif t == "strategy_regime_perf":
|
||||
_migrate_strategy_regime_perf(conn)
|
||||
elif t == "market_indicators":
|
||||
_migrate_market_indicators(conn)
|
||||
elif t == "regime_cycles":
|
||||
_migrate_regime_cycles(conn)
|
||||
conn.commit()
|
||||
print("全部完成。验证:")
|
||||
check_status(conn)
|
||||
conn.close()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -34,10 +34,13 @@ def load_all_strategies():
|
||||
return [r[0] for r in rows if r[0]]
|
||||
|
||||
|
||||
def load_regime_map():
|
||||
def load_regime_map(market='a'):
|
||||
"""读取指定市场的 date→regime 映射(温区归因用)"""
|
||||
conn = sqlite3.connect(DB, timeout=30)
|
||||
conn.execute("PRAGMA busy_timeout=30000")
|
||||
rows = conn.execute("SELECT date, regime FROM market_regime").fetchall()
|
||||
rows = conn.execute(
|
||||
"SELECT date, regime FROM market_regime WHERE market=?", (market,)
|
||||
).fetchall()
|
||||
conn.close()
|
||||
return dict(rows)
|
||||
|
||||
@@ -96,15 +99,22 @@ def calc_extra(trades):
|
||||
|
||||
|
||||
def main():
|
||||
regime_map = load_regime_map()
|
||||
# --market=a|hk:策略温区归因市场。当前所有策略为 A 股(market='a' 默认),
|
||||
# HK 维度留给阶段4(港股策略落地后按 strategy→market 映射归因)
|
||||
market = "a"
|
||||
for a in sys.argv[1:]:
|
||||
if a.startswith("--market="):
|
||||
market = a.split("=", 1)[1]
|
||||
regime_map = load_regime_map(market)
|
||||
strategies = load_all_strategies()
|
||||
print(f"策略数: {len(strategies)}")
|
||||
print(f"市场: {market} | 策略数: {len(strategies)}")
|
||||
|
||||
conn = sqlite3.connect(DB, timeout=60)
|
||||
conn.execute("PRAGMA busy_timeout=60000")
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS strategy_regime_perf (
|
||||
strategy TEXT,
|
||||
market TEXT NOT NULL DEFAULT 'a',
|
||||
regime TEXT,
|
||||
trades INTEGER,
|
||||
win_rate REAL,
|
||||
@@ -118,10 +128,10 @@ def main():
|
||||
sharpe_ratio REAL,
|
||||
profit_factor REAL,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (strategy, regime)
|
||||
PRIMARY KEY (strategy, regime, market)
|
||||
)
|
||||
""")
|
||||
conn.execute("DELETE FROM strategy_regime_perf")
|
||||
conn.execute("DELETE FROM strategy_regime_perf WHERE market=?", (market,))
|
||||
|
||||
written = 0
|
||||
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
@@ -144,11 +154,11 @@ def main():
|
||||
continue
|
||||
conn.execute(
|
||||
"""INSERT OR REPLACE INTO strategy_regime_perf
|
||||
(strategy, regime, trades, win_rate, avg_pnl, avg_hold_days,
|
||||
(strategy, market, regime, trades, win_rate, avg_pnl, avg_hold_days,
|
||||
total_return_pct, cagr_pct, portfolio_max_dd_pct, capital_final,
|
||||
positions_taken, sharpe_ratio, profit_factor, updated_at)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(v, reg, len(reg_trades),
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(v, market, reg, len(reg_trades),
|
||||
extra.get("win_rate"), extra.get("avg_pnl"), extra.get("avg_hold_days"),
|
||||
sim.get("total_return_pct"), sim.get("cagr_pct"), sim.get("portfolio_max_dd_pct"),
|
||||
sim.get("capital_final"), sim.get("positions_taken"),
|
||||
@@ -157,13 +167,14 @@ def main():
|
||||
written += 1
|
||||
conn.commit()
|
||||
conn.close()
|
||||
print(f"写入 strategy_regime_perf {written} 条(含温区级组合模拟)")
|
||||
print(f"写入 strategy_regime_perf {written} 条(market={market},含温区级组合模拟)")
|
||||
|
||||
# 打印样例
|
||||
conn = sqlite3.connect(DB, timeout=30)
|
||||
rows = conn.execute(
|
||||
"SELECT strategy, regime, trades, win_rate, cagr_pct, capital_final FROM strategy_regime_perf "
|
||||
"WHERE strategy IN ('v_oversold','v_mr_sel','s2_panic') ORDER BY strategy, regime"
|
||||
"WHERE strategy IN ('v_oversold','v_mr_sel','s2_panic') AND market=? ORDER BY strategy, regime",
|
||||
(market,)
|
||||
).fetchall()
|
||||
conn.close()
|
||||
for r in rows:
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
- 温区周期记录:regime_cycles 表(start/end/regime/days),供策略评估归因
|
||||
- 温度(rsi):不滞后,实时反映恐慌/亢奋(与温区互补:温区滞后、温度实时)
|
||||
|
||||
写表: regime_cycles(date_start, date_end, regime, days)
|
||||
输出: market_regime_smoothed.json(当前平滑温区 + 温度)
|
||||
写表: regime_cycles(market, start_date, end_date, regime, days) # 2026-08-14 阶段3 加 market 维度
|
||||
输出: market_regime_smoothed.json(当前平滑温区 + 温度;A股顶层键向后兼容 + markets 双市场详情)
|
||||
"""
|
||||
import sys
|
||||
import json
|
||||
@@ -27,11 +27,12 @@ OUT = "/home/hmo/MoFin/data/market_regime_smoothed.json"
|
||||
# 滞回确认天数(数据选参:K=5 甜区)
|
||||
CONFIRM_DAYS = 5
|
||||
|
||||
def load_daily_regime():
|
||||
"""读取 market_regime 逐日数据(时间正序)"""
|
||||
def load_daily_regime(market='a'):
|
||||
"""读取指定市场 market_regime 逐日数据(时间正序)"""
|
||||
conn = sqlite3.connect(DB, timeout=5)
|
||||
rows = conn.execute(
|
||||
"SELECT date, above_ma20, adx, regime FROM market_regime ORDER BY date ASC"
|
||||
"SELECT date, above_ma20, adx, regime FROM market_regime "
|
||||
"WHERE market=? ORDER BY date ASC", (market,)
|
||||
).fetchall()
|
||||
conn.close()
|
||||
return rows
|
||||
@@ -81,13 +82,14 @@ def smooth_states(rows, k=CONFIRM_DAYS):
|
||||
cycles.append(cur)
|
||||
return states, cycles, dates
|
||||
|
||||
def save_cycles(cycles):
|
||||
"""写入 regime_cycles 表"""
|
||||
def save_cycles(cycles, market='a'):
|
||||
"""写入 regime_cycles 表(按市场重建:A/港股各自独立周期,互不清除)"""
|
||||
conn = sqlite3.connect(DB, timeout=30)
|
||||
conn.execute("PRAGMA busy_timeout=30000")
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS regime_cycles (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
market TEXT NOT NULL DEFAULT 'a',
|
||||
regime TEXT,
|
||||
start_date TEXT,
|
||||
end_date TEXT,
|
||||
@@ -95,58 +97,82 @@ def save_cycles(cycles):
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
# 清空重建(保持与 market_regime 同步)
|
||||
conn.execute("DELETE FROM regime_cycles")
|
||||
# 只清本市场周期(保持与 market_regime 同步)
|
||||
conn.execute("DELETE FROM regime_cycles WHERE market=?", (market,))
|
||||
for cy in cycles:
|
||||
conn.execute(
|
||||
"INSERT INTO regime_cycles (regime, start_date, end_date, days) VALUES (?,?,?,?)",
|
||||
(cy["regime"], cy["start"], cy["end"], cy["days"])
|
||||
"INSERT INTO regime_cycles (market, regime, start_date, end_date, days) VALUES (?,?,?,?,?)",
|
||||
(market, cy["regime"], cy["start"], cy["end"], cy["days"])
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return len(cycles)
|
||||
|
||||
def get_temp():
|
||||
"""实时温度(rsi 档位,不滞后)"""
|
||||
def get_temp(market='a'):
|
||||
"""实时温度(rsi 档位,不滞后),按市场"""
|
||||
try:
|
||||
from temp_band import get_market_temp
|
||||
return get_market_temp()
|
||||
return get_market_temp(market=market)
|
||||
except Exception:
|
||||
return {"band": "unknown", "rsi": None}
|
||||
|
||||
def main():
|
||||
rows = load_daily_regime()
|
||||
def get_smoothed_regime(market='a'):
|
||||
"""计算指定市场当前平滑温区(K=5 滞回确认)。
|
||||
返回 {current_regime, current_date, states, cycles, dates, total_cycles, recent_cycles};
|
||||
数据不足(<K+1)时返回 None。"""
|
||||
rows = load_daily_regime(market)
|
||||
if len(rows) < CONFIRM_DAYS + 1:
|
||||
print(f"数据不足: {len(rows)} 条")
|
||||
return None
|
||||
states, cycles, dates = smooth_states(rows)
|
||||
return {
|
||||
"current_regime": states[-1],
|
||||
"current_date": dates[-1],
|
||||
"states": states,
|
||||
"cycles": cycles,
|
||||
"dates": dates,
|
||||
"total_cycles": len(cycles),
|
||||
"recent_cycles": cycles[-8:],
|
||||
}
|
||||
|
||||
def main():
|
||||
details = {}
|
||||
for market in ("a", "hk"):
|
||||
r = get_smoothed_regime(market)
|
||||
if not r:
|
||||
print(f"[regime_tracker] {market} 数据不足: 不足{CONFIRM_DAYS + 1}条,跳过", flush=True)
|
||||
continue
|
||||
n_cycles = save_cycles(r["cycles"], market=market)
|
||||
details[market] = {
|
||||
"current_regime": r["current_regime"],
|
||||
"current_date": r["current_date"],
|
||||
"confirm_days": CONFIRM_DAYS,
|
||||
"temp": get_temp(market),
|
||||
"total_cycles": n_cycles,
|
||||
"recent_cycles": r["recent_cycles"],
|
||||
}
|
||||
print(f"[{market}] 平滑温区: {r['current_regime']} (确认期{CONFIRM_DAYS}天, 至{r['current_date']})")
|
||||
print(f"[{market}] 温度: {details[market]['temp'].get('band')} (rsi={details[market]['temp'].get('rsi')})")
|
||||
print(f"[{market}] 周期总数: {n_cycles}")
|
||||
for cy in r["recent_cycles"]:
|
||||
print(f" {cy['regime']:<12} {cy['start']} ~ {cy['end']} ({cy['days']}天)")
|
||||
|
||||
if not details:
|
||||
print("两个市场数据都不足,未生成输出", flush=True)
|
||||
return
|
||||
|
||||
states, cycles, dates = smooth_states(rows)
|
||||
n_cycles = save_cycles(cycles)
|
||||
|
||||
# 当前平滑温区(最新确认态)
|
||||
current_regime = states[-1]
|
||||
current_date = dates[-1]
|
||||
temp = get_temp()
|
||||
|
||||
# 最近周期列表
|
||||
recent = cycles[-8:]
|
||||
# 顶层键保持 A 股(向后兼容 regime_gate/strategy_router 读 current_regime/temp/recent_cycles)
|
||||
a = details.get("a") or {}
|
||||
out = {
|
||||
"current_regime": current_regime,
|
||||
"current_date": current_date,
|
||||
"current_regime": a.get("current_regime"),
|
||||
"current_date": a.get("current_date"),
|
||||
"confirm_days": CONFIRM_DAYS,
|
||||
"temp": temp,
|
||||
"total_cycles": n_cycles,
|
||||
"recent_cycles": recent,
|
||||
"temp": a.get("temp"),
|
||||
"total_cycles": a.get("total_cycles"),
|
||||
"recent_cycles": a.get("recent_cycles"),
|
||||
"markets": details, # 双市场详情(A 股 + 港股)
|
||||
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
}
|
||||
Path(OUT).write_text(json.dumps(out, ensure_ascii=False, indent=1), encoding="utf-8")
|
||||
|
||||
print(f"平滑温区: {current_regime} (确认期{CONFIRM_DAYS}天, 至{current_date})")
|
||||
print(f"温度: {temp.get('band')} (rsi={temp.get('rsi')})")
|
||||
print(f"周期总数: {n_cycles}")
|
||||
print("最近周期:")
|
||||
for cy in recent:
|
||||
print(f" {cy['regime']:<12} {cy['start']} ~ {cy['end']} ({cy['days']}天)")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -20,8 +20,10 @@
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
|
||||
from market_config import MARKETS
|
||||
|
||||
DB = Path("/home/hmo/MoFin/data/mofin.db")
|
||||
INDEX = "sh000001"
|
||||
INDEX = "sh000001" # A股默认指数(兜底);实际以 MARKETS[market]['index_code'] 为准
|
||||
|
||||
|
||||
def calc_rsi(series, n=14):
|
||||
@@ -69,19 +71,25 @@ def temp_multiplier(band, strategy_family="mr"):
|
||||
}.get(band, 0.5)
|
||||
|
||||
|
||||
def get_market_temp(db_path=None):
|
||||
"""读取最新市场温度(rsi + 档位 + regime)。"""
|
||||
def get_market_temp(market='a', db_path=None):
|
||||
"""读取指定市场最新温度(rsi + 档位 + regime)。
|
||||
|
||||
market: 'a'(默认, A股 sh000001) / 'hk'(港股 hkHSI)。
|
||||
指数代码从 market_config.MARKETS 读取,不写死。
|
||||
"""
|
||||
db = db_path or DB
|
||||
index_code = MARKETS.get(market, MARKETS['a'])['index_code']
|
||||
conn = sqlite3.connect(str(db), timeout=5)
|
||||
try:
|
||||
# 上证最近 30 日收盘算 rsi
|
||||
# 指数最近 30 日收盘算 rsi
|
||||
rows = conn.execute(
|
||||
"SELECT date, close FROM stock_daily WHERE code=? ORDER BY date DESC LIMIT 30",
|
||||
(INDEX,)
|
||||
(index_code,)
|
||||
).fetchall()
|
||||
# 最新 regime
|
||||
# 最新 regime(按市场过滤——否则 A 股温度会误读港股 regime)
|
||||
reg = conn.execute(
|
||||
"SELECT date, regime FROM market_regime ORDER BY date DESC LIMIT 1"
|
||||
"SELECT date, regime FROM market_regime WHERE market=? ORDER BY date DESC LIMIT 1",
|
||||
(market,)
|
||||
).fetchone()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
Reference in New Issue
Block a user