217 lines
8.7 KiB
Python
217 lines
8.7 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""2026-08-16 资格评估与可用性管理(数据层)
|
||
标准 A(老莫确认):
|
||
长期合格 = 适应温区 10y 年化 > 大盘10y
|
||
近期不错 = 适应温区 2y 年化 > 大盘2y
|
||
当下能打 = 适应温区 1y 年化 > 大盘1y
|
||
三项全过 → 激活资格;长期+近期 → 可用;长期不过 → 历史
|
||
可用性(manual_available)由老莫手动把关:激活只能是可用的策略。
|
||
新策略按默认标准(A)初始化可用性;定期评估自动标记「可用性下降/回升」警告。
|
||
|
||
存储:data/strategy_availability.json(git 可追踪,同 strategy_weights.json 模式)
|
||
"""
|
||
import json
|
||
import os
|
||
import sqlite3
|
||
from datetime import datetime, timedelta
|
||
from pathlib import Path
|
||
|
||
DATA_DIR = Path("/home/hmo/MoFin/data")
|
||
AVAIL_JSON = DATA_DIR / "strategy_availability.json"
|
||
|
||
# 大盘基准(market_regime 指数年化,按需现算)
|
||
BENCH_DAYS = {"1y": 365, "2y": 730, "10y": 3650}
|
||
|
||
|
||
def get_benchmarks(market="a"):
|
||
"""大盘年化基准:1y/2y/10y"""
|
||
out = {}
|
||
try:
|
||
conn = sqlite3.connect(str(DATA_DIR / "mofin.db"), timeout=5)
|
||
for label, days in BENCH_DAYS.items():
|
||
cutoff = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
|
||
last = conn.execute(
|
||
"SELECT close FROM market_regime WHERE market=? AND close IS NOT NULL ORDER BY date DESC LIMIT 1",
|
||
(market,)).fetchone()
|
||
base = conn.execute(
|
||
"SELECT close FROM market_regime WHERE market=? AND close IS NOT NULL AND date>=? ORDER BY date LIMIT 1",
|
||
(market, cutoff)).fetchone()
|
||
if last and base and base[0]:
|
||
pct = (last[0] / base[0] - 1) * 100
|
||
yrs = days / 365.0
|
||
if pct > -100:
|
||
out[label] = round(((1 + pct / 100) ** (1 / yrs) - 1) * 100, 1)
|
||
conn.close()
|
||
except Exception:
|
||
pass
|
||
return out
|
||
|
||
|
||
def evaluate_strategy(version, market="a", best_regime=None, bench=None):
|
||
"""评估单策略资格:长期/近期/当下 三项达标
|
||
返回 {long_ok, mid_ok, short_ok, cagr_10y, cagr_2y, cagr_1y, regime}
|
||
"""
|
||
bench = bench or get_benchmarks(market)
|
||
if not bench:
|
||
return None
|
||
try:
|
||
conn = sqlite3.connect(str(DATA_DIR / "mofin.db"), timeout=5)
|
||
conn.row_factory = sqlite3.Row
|
||
# 适应温区:best_regime 传入或用 10y 最高胜率温区
|
||
if not best_regime:
|
||
rows = conn.execute(
|
||
"SELECT regime, win_rate FROM strategy_regime_perf_by_period "
|
||
"WHERE strategy=? AND market=? AND period_tag='10y'",
|
||
(version, market)).fetchall()
|
||
if not rows:
|
||
conn.close()
|
||
return None
|
||
best_regime = max(rows, key=lambda x: x[1] or 0)[0]
|
||
# 各周期适应温区年化
|
||
cagr = {}
|
||
for pt in ["1y", "2y", "10y"]:
|
||
r = conn.execute(
|
||
"SELECT cagr_pct, win_rate, trades FROM strategy_regime_perf_by_period "
|
||
"WHERE strategy=? AND market=? AND regime=? AND period_tag=?",
|
||
(version, market, best_regime, pt)).fetchone()
|
||
cagr[pt] = (r["cagr_pct"], r["win_rate"], r["trades"]) if r else (None, None, None)
|
||
conn.close()
|
||
except Exception:
|
||
return None
|
||
c10, w10, t10 = cagr.get("10y", (None, None, None))
|
||
c2, w2, t2 = cagr.get("2y", (None, None, None))
|
||
c1, w1, t1 = cagr.get("1y", (None, None, None))
|
||
return {
|
||
"regime": best_regime,
|
||
"cagr_10y": c10, "cagr_2y": c2, "cagr_1y": c1,
|
||
"trades_10y": t10, "trades_2y": t2, "trades_1y": t1,
|
||
"long_ok": c10 is not None and c10 > bench.get("10y", 0),
|
||
"mid_ok": c2 is not None and c2 > bench.get("2y", 0),
|
||
"short_ok": c1 is not None and c1 > bench.get("1y", 0),
|
||
}
|
||
|
||
|
||
def evaluate_all_regimes(version, market="a", bench=None):
|
||
"""评估策略在【每个有数据温区】的资格(2026-08-16 老莫:策略可适应多个温区)
|
||
返回 {regime: {long_ok, mid_ok, short_ok, cagr_10y, cagr_2y, cagr_1y, trades_10y, trades_2y, trades_1y}}
|
||
只包含温区数据存在(有 10y 记录)的温区
|
||
"""
|
||
bench = bench or get_benchmarks(market)
|
||
if not bench:
|
||
return {}
|
||
out = {}
|
||
try:
|
||
conn = sqlite3.connect(str(DATA_DIR / "mofin.db"), timeout=5)
|
||
conn.row_factory = sqlite3.Row
|
||
# 该策略所有温区(有 10y 记录才算数)
|
||
regimes = [r[0] for r in conn.execute(
|
||
"SELECT DISTINCT regime FROM strategy_regime_perf_by_period "
|
||
"WHERE strategy=? AND market=? AND period_tag='10y'",
|
||
(version, market)).fetchall()]
|
||
for rg in regimes:
|
||
cagr = {}
|
||
for pt in ["1y", "2y", "10y"]:
|
||
r = conn.execute(
|
||
"SELECT cagr_pct, win_rate, trades FROM strategy_regime_perf_by_period "
|
||
"WHERE strategy=? AND market=? AND regime=? AND period_tag=?",
|
||
(version, market, rg, pt)).fetchone()
|
||
cagr[pt] = (r["cagr_pct"], r["win_rate"], r["trades"]) if r else (None, None, None)
|
||
c10, w10, t10 = cagr.get("10y", (None, None, None))
|
||
c2, w2, t2 = cagr.get("2y", (None, None, None))
|
||
c1, w1, t1 = cagr.get("1y", (None, None, None))
|
||
out[rg] = {
|
||
"cagr_10y": c10, "cagr_2y": c2, "cagr_1y": c1,
|
||
"trades_10y": t10, "trades_2y": t2, "trades_1y": t1,
|
||
"long_ok": c10 is not None and c10 > bench.get("10y", 0),
|
||
"mid_ok": c2 is not None and c2 > bench.get("2y", 0),
|
||
"short_ok": c1 is not None and c1 > bench.get("1y", 0),
|
||
}
|
||
conn.close()
|
||
except Exception:
|
||
pass
|
||
return out
|
||
|
||
|
||
def load_availability():
|
||
"""读手动可用性状态 {version: {available: bool, note, updated_at}}"""
|
||
try:
|
||
if AVAIL_JSON.exists():
|
||
return json.loads(AVAIL_JSON.read_text(encoding="utf-8"))
|
||
except Exception:
|
||
pass
|
||
return {}
|
||
|
||
|
||
def save_availability(data):
|
||
"""写手动可用性状态"""
|
||
AVAIL_JSON.write_text(json.dumps(data, ensure_ascii=False, indent=1), encoding="utf-8")
|
||
|
||
|
||
def _strategy_market(version):
|
||
"""探测策略市场:strategy_research.market('hk'=港股,否则 A股)"""
|
||
try:
|
||
conn = sqlite3.connect(str(DATA_DIR / "mofin.db"), timeout=5)
|
||
r = conn.execute(
|
||
"SELECT market FROM strategy_research WHERE version=? AND COALESCE(market,'')!='' "
|
||
"ORDER BY id DESC LIMIT 1", (version,)).fetchone()
|
||
conn.close()
|
||
if r and r[0] == "hk":
|
||
return "hk"
|
||
except Exception:
|
||
pass
|
||
return "a"
|
||
|
||
|
||
def auto_init_availability(versions, bench=None, market_by_version=None):
|
||
"""新策略按标准 A 自动初始化可用性(一般默认可用;长期不合格则不可用)
|
||
已存在的手动状态不动。返回新增的默认可用策略。
|
||
market_by_version: dict {version: 'a'|'hk'},缺省按 strategy_research.market 探测
|
||
"""
|
||
bench = bench or get_benchmarks()
|
||
avail = load_availability()
|
||
changed = []
|
||
for v in versions:
|
||
if v in avail:
|
||
continue
|
||
mkt = (market_by_version or {}).get(v) or _strategy_market(v)
|
||
ev = evaluate_strategy(v, mkt, bench=bench)
|
||
# 默认:长期合格即可用(A 标准);长期不合格(10y跑不赢大盘)默认不可用
|
||
# 无评估数据(如港股策略在 hk 也无温区数据)→ 默认可用(不误杀新策略)
|
||
default_avail = True if ev is None else bool(ev["long_ok"])
|
||
if ev is None:
|
||
note = "自动初始化:无温区数据,默认可用"
|
||
elif default_avail:
|
||
note = "自动初始化(标准A)"
|
||
else:
|
||
note = "自动初始化:长期不合格(10y<大盘)"
|
||
avail[v] = {
|
||
"available": default_avail,
|
||
"note": note,
|
||
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||
}
|
||
changed.append((v, default_avail))
|
||
if changed:
|
||
save_availability(avail)
|
||
return changed
|
||
|
||
|
||
def is_available(version):
|
||
"""查询手动可用性(默认 true——新策略默认可用,除非明确标不可用)"""
|
||
avail = load_availability()
|
||
entry = avail.get(version)
|
||
if entry is None:
|
||
return True # 未标记 = 默认可用
|
||
return bool(entry.get("available", True))
|
||
|
||
|
||
def set_available(version, available, note=""):
|
||
"""手动设置可用性"""
|
||
avail = load_availability()
|
||
avail[version] = {
|
||
"available": bool(available),
|
||
"note": note or ("手动可用" if available else "手动不可用"),
|
||
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||
}
|
||
save_availability(avail)
|
||
return avail[version]
|