Files

256 lines
11 KiB
Python
Raw Permalink 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.
# -*- coding: utf-8 -*-
"""2026-08-16 资格评估与可用性管理(数据层)
标准 A(老莫确认):
长期合格 = 适应温区 10y 年化 > 大盘10y
近期不错 = 适应温区 2y 年化 > 大盘2y
当下能打 = 适应温区 1y 年化 > 大盘1y
三项全过 → 激活资格;长期+近期 → 可用;长期不过 → 历史
可用性(manual_available)由老莫手动把关:激活只能是可用的策略。
新策略按默认标准(A)初始化可用性;定期评估自动标记「可用性下降/回升」警告。
存储:data/strategy_availability.jsongit 可追踪,同 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
A股:全年大盘年化(market_regime 指数 close,复利年化)
港股(2026-08-16 老莫确认):下跌市时段基准——trend_down 日期的 HSI 表现,
线性放大年化(trend_down 时段收益 × 365/td天数)。港股策略只在下跌市运行,与全年比不公平。
"""
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)
# ── 港股:下跌市时段基准(trend_down 期间 HSI,按每次时段分段算再合并)──
if market == "hk":
out = {}
for label, days in BENCH_DAYS.items():
cutoff = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
rows = conn.execute(
"SELECT date, regime, close FROM market_regime WHERE market='hk' AND date>=? "
"ORDER BY date", (cutoff,)).fetchall()
# 分段:连续 trend_down 日组成段,段内 HSI 末/首 -1
closes = {r[0]: r[2] for r in rows}
seg_ret = []
in_seg = False
seg_first = None
seg_last_close = None
for d, reg, c in rows:
if reg == "trend_down" and c:
if not in_seg:
in_seg = True
seg_first = c
seg_last_close = c
else:
if in_seg:
seg_ret.append((c / seg_first - 1) * 100 if c and seg_first else 0)
in_seg = False
seg_first = None
seg_last_close = None
if in_seg and seg_first:
# 最后一段未结束(窗口尾部仍下跌市),用段内最后 close
if seg_last_close:
seg_ret.append((seg_last_close / seg_first - 1) * 100)
if not seg_ret:
continue
# 合并:段收益加总(下跌市期间累计表现),线性年化 ×365/窗口天数
total_ret = sum(seg_ret)
out[label] = round(total_ret * (365.0 / max(days, 1)), 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]