diff --git a/evolution/__init__.py b/evolution/__init__.py new file mode 100644 index 00000000..42d799b7 --- /dev/null +++ b/evolution/__init__.py @@ -0,0 +1,70 @@ +""" +evolution/__init__.py — 自我进化模块 +Loop Engineering: 策略健康度监控 + 教训提取 + 自动迭代 +""" +import sqlite3, os + +DB = os.environ.get('MOFIN_DB', '/home/hmo/MoFin/data/mofin.db') + +def init_evolution_tables(conn=None): + """初始化进化模块数据表""" + close_conn = False + if conn is None: + conn = sqlite3.connect(DB) + close_conn = True + + # 策略健康度每日快照 + conn.execute(""" + CREATE TABLE IF NOT EXISTS strategy_health ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + strategy_version TEXT NOT NULL, + date TEXT NOT NULL, + live_trades INTEGER DEFAULT 0, + live_wins INTEGER DEFAULT 0, + live_return_pct REAL DEFAULT 0, + backtest_wr REAL DEFAULT 0, + backtest_avg_ret REAL DEFAULT 0, + deviation REAL DEFAULT 0, + health_score REAL DEFAULT 0, + created_at TEXT DEFAULT (datetime('now','localtime')), + UNIQUE(strategy_version, date) + ) + """) + conn.execute("CREATE INDEX IF NOT EXISTS idx_health_version_date ON strategy_health(strategy_version, date)") + + # 教训库 + conn.execute(""" + CREATE TABLE IF NOT EXISTS strategy_lessons ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + strategy_version TEXT NOT NULL, + trade_id INTEGER, + lesson_type TEXT NOT NULL, + lesson_text TEXT NOT NULL, + confidence REAL DEFAULT 0.5, + applied INTEGER DEFAULT 0, + created_at TEXT DEFAULT (datetime('now','localtime')) + ) + """) + conn.execute("CREATE INDEX IF NOT EXISTS idx_lessons_version ON strategy_lessons(strategy_version, applied)") + + # 策略迭代历史 + conn.execute(""" + CREATE TABLE IF NOT EXISTS strategy_evolution ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + parent_version TEXT NOT NULL, + child_version TEXT NOT NULL, + change_description TEXT, + backtest_result TEXT, + promoted INTEGER DEFAULT 0, + created_at TEXT DEFAULT (datetime('now','localtime')) + ) + """) + conn.execute("CREATE INDEX IF NOT EXISTS idx_evolution_parent ON strategy_evolution(parent_version, promoted)") + + conn.commit() + if close_conn: + conn.close() + +if __name__ == '__main__': + init_evolution_tables() + print("进化模块数据表初始化完成") diff --git a/evolution/auto_iterator.py b/evolution/auto_iterator.py new file mode 100644 index 00000000..ba283486 --- /dev/null +++ b/evolution/auto_iterator.py @@ -0,0 +1,125 @@ +""" +evolution/auto_iterator.py — 策略自动迭代 +健康度低时生成参数变体,跑回测,记录结果 +""" +import sys, os, json, sqlite3, copy +from datetime import datetime + +sys.path.insert(0, '/home/hmo/MoFin') +import strategy_lab as lab + +DB = '/home/hmo/MoFin/data/mofin.db' + +# 可迭代的参数空间 +PARAM_SPACE = { + 'max_hold_days': [40, 60, 80, 100, 120], + 'reentry_days': [5, 10, 15, 20], + 'sl_atr': [1.0, 1.5, 2.0, 2.5], +} + + +def get_current_health(version='v_next4', days=7): + """获取当前策略健康度""" + conn = sqlite3.connect(DB) + conn.row_factory = sqlite3.Row + rows = conn.execute(""" + SELECT health_score, date FROM strategy_health + WHERE strategy_version=? ORDER BY date DESC LIMIT ? + """, (version, days)).fetchall() + conn.close() + if not rows: + return 50 # 无数据,中性 + return round(sum(r['health_score'] for r in rows) / len(rows), 1) + + +def propose_variants(parent_version, health): + """根据健康度生成变体参数建议""" + if health >= 70: + print(f"健康度{health}≥70,无需迭代", flush=True) + return [] + + variants = [] + severity = 'minor' if health >= 50 else 'major' + + if severity == 'minor': + # 小幅调参 + variants.append({ + 'parent': parent_version, + 'params': {'max_hold_days': 80, 'reentry_days': 15, 'sl_atr': 1.5}, + 'description': '微调:确保当前最优参数', + }) + else: + # 大幅调参(扫参数网格) + base_hold = 60 + base_reentry = 10 + for hold in PARAM_SPACE['max_hold_days']: + for reentry in PARAM_SPACE['reentry_days']: + variants.append({ + 'parent': parent_version, + 'params': {'max_hold_days': hold, 'reentry_days': reentry, 'sl_atr': 1.5}, + 'description': f'网格扫描: h{hold}/r{reentry}', + }) + + return variants + + +def test_variant(variant): + """测试单个变体""" + name = f"auto_h{variant['params']['max_hold_days']}_r{variant['params']['reentry_days']}" + # 克隆基座配置 + base = lab.STRATEGIES.get(variant['parent']) + if not base: + return None + cfg = copy.deepcopy(base) + cfg['version'] = name + cfg['name'] = f"自进化-{variant['description']}" + for k, v in variant['params'].items(): + cfg['config']['exit'][k] = v + lab.STRATEGIES[name] = cfg + + results = {} + for tag, start, end in [('5y', '2021-07-01', '2026-07-24')]: + r = lab.run_backtest(name, start, end, 913000, save=False, universe='a', period_tag=tag) + pf = r['summary'].get('portfolio_full', {}) + results[tag] = { + 'full': pf.get('total_return_pct'), + 'cagr': pf.get('cagr_pct'), + 'dd': pf.get('portfolio_max_dd_pct'), + } + return {'name': name, 'description': variant['description'], 'results': results} + + +def run_iteration(version='v_next4'): + """执行一次迭代检查""" + health = get_current_health(version) + print(f"{version} 健康度: {health}", flush=True) + + variants = propose_variants(version, health) + if not variants: + return [] + + conn = sqlite3.connect(DB) + results = [] + for v in variants[:3]: # 最多测3个 + print(f"测试: {v['description']}", flush=True) + r = test_variant(v) + if r: + results.append(r) + # 记录到 evolution 表 + conn.execute(""" + INSERT INTO strategy_evolution (parent_version, child_version, change_description, backtest_result, promoted) + VALUES (?, ?, ?, ?, 0) + """, (version, r['name'], r['description'], json.dumps(r['results'], ensure_ascii=False))) + conn.commit() + + # 打印对比 + for r in results: + rs = r['results'].get('5y', {}) + print(f" {r['name']}: full={rs.get('full')}% cagr={rs.get('cagr')}% dd={rs.get('dd')}%", flush=True) + + conn.close() + return results + + +if __name__ == '__main__': + run_iteration() diff --git a/evolution/evolution_api.py b/evolution/evolution_api.py new file mode 100644 index 00000000..b6055252 --- /dev/null +++ b/evolution/evolution_api.py @@ -0,0 +1,97 @@ +""" +evolution/evolution_api.py — 进化模块 API 接口 +供 dashboard 查询健康度、教训、迭代历史 +""" +import sys, os, json, sqlite3 + +sys.path.insert(0, '/home/hmo/MoFin') +sys.path.insert(0, '/home/hmo/MoFin/deploy/profile-scripts') + +DB = os.environ.get('MOFIN_DB', '/home/hmo/MoFin/data/mofin.db') + + +def get_evolution_dashboard(): + """进化模块 Dashboard 数据""" + conn = sqlite3.connect(DB) + conn.row_factory = sqlite3.Row + + # 最近健康度(近30天) + health = [] + for r in conn.execute(""" + SELECT strategy_version, date, live_trades, live_wins, live_return_pct, + backtest_wr, backtest_avg_ret, deviation, health_score + FROM strategy_health ORDER BY date DESC LIMIT 30 + """).fetchall(): + health.append(dict(r)) + + # 最近教训(近20条) + lessons = [] + for r in conn.execute(""" + SELECT strategy_version, lesson_type, lesson_text, confidence, applied, created_at + FROM strategy_lessons ORDER BY id DESC LIMIT 20 + """).fetchall(): + lessons.append(dict(r)) + + # 迭代历史 + evolution = [] + for r in conn.execute(""" + SELECT parent_version, child_version, change_description, promoted, created_at + FROM strategy_evolution ORDER BY id DESC LIMIT 20 + """).fetchall(): + evolution.append(dict(r)) + + # 当前策略基线 + baseline = {} + for v in ['v_next4', 'v_next3', 'v8.1']: + r = conn.execute(""" + SELECT results_json FROM strategy_research + WHERE version=? AND period_tag='5y' ORDER BY id DESC LIMIT 1 + """, (v,)).fetchone() + if r: + res = json.loads(r[0]) + s = res.get('summary', {}) + pf = s.get('portfolio_full', {}) + baseline[v] = { + 'win_rate': s.get('win_rate', 0), + 'total_return': pf.get('total_return_pct', 0), + 'cagr': pf.get('cagr_pct', 0), + 'max_dd': pf.get('portfolio_max_dd_pct', 0), + } + + conn.close() + + return { + 'health': health, + 'lessons': lessons, + 'evolution': evolution, + 'baseline': baseline, + } + + +def get_health_trend(version='v_next4', days=30): + """健康度趋势""" + conn = sqlite3.connect(DB) + conn.row_factory = sqlite3.Row + rows = conn.execute(""" + SELECT date, health_score, deviation, live_trades + FROM strategy_health WHERE strategy_version=? ORDER BY date DESC LIMIT ? + """, (version, days)).fetchall() + conn.close() + return [dict(r) for r in rows] + + +def record_evolution(parent, child, description, backtest_result=None, promoted=0): + """记录一次策略迭代""" + conn = sqlite3.connect(DB) + conn.execute(""" + INSERT INTO strategy_evolution (parent_version, child_version, change_description, backtest_result, promoted) + VALUES (?, ?, ?, ?, ?) + """, (parent, child, description, json.dumps(backtest_result) if backtest_result else None, promoted)) + conn.commit() + conn.close() + + +if __name__ == '__main__': + d = get_evolution_dashboard() + print(f"健康度: {len(d['health'])}条, 教训: {len(d['lessons'])}条, 迭代: {len(d['evolution'])}条") + print(f"基线: {list(d['baseline'].keys())}") diff --git a/evolution/health_monitor.py b/evolution/health_monitor.py new file mode 100644 index 00000000..0649bec5 --- /dev/null +++ b/evolution/health_monitor.py @@ -0,0 +1,142 @@ +""" +evolution/health_monitor.py — 策略健康度监控 +对比实盘交易 vs 回测预期,计算健康分,偏差过大时报警 +""" +import sys, os, json, sqlite3 +from datetime import datetime, timedelta + +sys.path.insert(0, '/home/hmo/MoFin') +sys.path.insert(0, '/home/hmo/MoFin/deploy/profile-scripts') + +DB = os.environ.get('MOFIN_DB', '/home/hmo/MoFin/data/mofin.db') +CURRENT_STRATEGY = 'v_next4' + + +def get_backtest_baseline(conn, version): + """从 strategy_research 取回测基线""" + r = conn.execute(""" + SELECT results_json FROM strategy_research + WHERE version=? AND period_tag='5y' ORDER BY id DESC LIMIT 1 + """, (version,)).fetchone() + if not r: + return None + res = json.loads(r[0]) + s = res.get('summary', {}) + return { + 'win_rate': s.get('win_rate', 0), + 'avg_profit_pct': s.get('avg_profit_pct', 0), + 'total_trades': s.get('total_trades', 0), + } + + +def get_live_trades(conn, days=7): + """取近N天实盘交易(holding_strategies 全部,计算盈亏)""" + since = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d') + try: + rows = conn.execute(""" + SELECT code, name, price as current_price, avg_price as entry_price, + timing_signal as signal, updated_at, + CASE WHEN avg_price > 0 THEN round((price - avg_price) / avg_price * 100, 2) ELSE 0 END as profit_pct + FROM holding_strategies + WHERE updated_at >= ? AND avg_price > 0 + ORDER BY updated_at DESC + """, (since,)).fetchall() + return rows + except sqlite3.OperationalError as e: + print(f"查询失败: {e}", flush=True) + return [] + + +def calc_health_score(live_wr, live_ret, backtest_wr, backtest_ret): + """计算健康分 (0-100) + 健康分 = 100 - 偏差惩罚 + 偏差 = |实盘胜率-回测胜率| + |实盘收益-回测收益|/2 + """ + if backtest_wr == 0: + return 50 # 无基线,中性分 + + wr_dev = abs(live_wr - backtest_wr) + ret_dev = abs(live_ret - backtest_ret) / 2 + deviation = wr_dev + ret_dev + + # 偏差越大,健康分越低 + health = max(0, 100 - deviation * 2) + return round(health, 1) + + +def run_health_check(strategy_version=None): + """执行健康度检查""" + version = strategy_version or CURRENT_STRATEGY + conn = sqlite3.connect(DB) + conn.row_factory = sqlite3.Row + + # 回测基线 + baseline = get_backtest_baseline(conn, version) + if not baseline: + print(f"无 {version} 回测基线", flush=True) + conn.close() + return None + + # 实盘交易(近7天) + live = get_live_trades(conn, days=7) + today = datetime.now().strftime('%Y-%m-%d') + + if not live: + # 无实盘数据,记录中性健康分 + health = 50 + deviation = 0 + live_wr = live_ret = 0 + print(f"{version}: 近7天无实盘交易,健康分=50(中性)", flush=True) + else: + wins = sum(1 for t in live if (t.get('profit_pct') or 0) > 0) + total = len(live) + live_wr = round(100 * wins / total, 1) if total else 0 + live_ret = round(sum(t.get('profit_pct') or 0 for t in live) / total, 2) if total else 0 + + health = calc_health_score(live_wr, live_ret, baseline['win_rate'], baseline['avg_profit_pct']) + deviation = abs(live_wr - baseline['win_rate']) + + # 写入 strategy_health 表 + conn.execute(""" + INSERT OR REPLACE INTO strategy_health + (strategy_version, date, live_trades, live_wins, live_return_pct, + backtest_wr, backtest_avg_ret, deviation, health_score) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + """, (version, today, len(live), sum(1 for t in live if (t.get('profit_pct') or 0) > 0), + live_ret, baseline['win_rate'], baseline['avg_profit_pct'], deviation, health)) + conn.commit() + + # 报警判断 + alert = None + if health < 40: + alert = f"🔴 策略健康度严重下降: {health}分 (偏差{deviation}pp)" + elif health < 60: + alert = f"🟡 策略健康度偏低: {health}分 (偏差{deviation}pp)" + + result = { + 'version': version, + 'date': today, + 'live_trades': len(live), + 'live_wr': live_wr, + 'live_ret': live_ret, + 'backtest_wr': baseline['win_rate'], + 'backtest_ret': baseline['avg_profit_pct'], + 'deviation': deviation, + 'health_score': health, + 'alert': alert, + } + + print(f"{version} 健康度: {health}分 (实盘{live_wr}%/{live_ret}% vs 回测{baseline['win_rate']}%/{baseline['avg_profit_pct']}%)", flush=True) + if alert: + print(f" {alert}", flush=True) + + conn.close() + return result + + +if __name__ == '__main__': + import sys + sys.path.insert(0, '/home/hmo/MoFin/evolution') + from __init__ import init_evolution_tables + init_evolution_tables() + run_health_check() diff --git a/evolution/lesson_extractor.py b/evolution/lesson_extractor.py new file mode 100644 index 00000000..7de562c9 --- /dev/null +++ b/evolution/lesson_extractor.py @@ -0,0 +1,106 @@ +""" +evolution/lesson_extractor.py — 交易后教训提取 +分析已平仓交易,用LLM提取"为什么赢/亏"的教训 +""" +import sys, os, json, sqlite3 +from datetime import datetime, timedelta + +sys.path.insert(0, '/home/hmo/MoFin') +sys.path.insert(0, '/home/hmo/MoFin/deploy/profile-scripts') + +DB = os.environ.get('MOFIN_DB', '/home/hmo/MoFin/data/mofin.db') + + +def get_closed_trades(conn, days=30): + """取近N天已平仓的交易(从strategy_research的trades里取最近的,或从holding_strategies推断)""" + # 从最近的回测结果取交易(作为样本分析) + r = conn.execute(""" + SELECT results_json FROM strategy_research + WHERE version='v_next4' AND period_tag='5y' ORDER BY id DESC LIMIT 1 + """).fetchone() + if not r: + return [] + res = json.loads(r[0]) + trades = res.get('trades', []) + # 按日期排序,取最近的 + recent = sorted(trades, key=lambda x: x.get('entry_date', ''), reverse=True)[:10] + return recent + + +def analyze_trade(trade): + """分析单笔交易的成败原因(规则化,非LLM)""" + profit = trade.get('profit_pct', 0) + hold_days = trade.get('hold_days', 0) + factors = trade.get('factors', {}) + + lessons = [] + + # 盈利交易的共性 + if profit > 15: + if factors.get('mkt_adx', 0) > 25: + lessons.append(('win_pattern', f"大盘趋势强(ADX={factors['mkt_adx']:.0f})时盈利{profit:.1f}%", 0.8)) + if factors.get('sector_above_ma20'): + lessons.append(('win_pattern', f"板块在MA20上方时盈利{profit:.1f}%", 0.7)) + if trade.get('dna'): + lessons.append(('win_pattern', f"动量基因(DNA)票盈利{profit:.1f}%", 0.9)) + + # 亏损交易的共性 + if profit < -5: + if factors.get('mkt_slope', 0) < -0.5: + lessons.append(('loss_pattern', f"大盘斜率负({factors['mkt_slope']:.2f})时亏损{profit:.1f}%", 0.7)) + if not factors.get('sector_above_ma20'): + lessons.append(('loss_pattern', f"板块在MA20下方时亏损{profit:.1f}%", 0.6)) + if hold_days < 5: + lessons.append(('loss_pattern', f"持仓{hold_days}天短于5天时亏损{profit:.1f}%", 0.5)) + + # 长持盈利 + if profit > 10 and hold_days > 30: + lessons.append(('win_pattern', f"长持{hold_days}天盈利{profit:.1f}%", 0.85)) + + return lessons + + +def extract_lessons(days=30): + """提取近N天交易的教训""" + conn = sqlite3.connect(DB) + conn.row_factory = sqlite3.Row + + trades = get_closed_trades(conn, days) + if not trades: + print("无交易数据", flush=True) + conn.close() + return [] + + all_lessons = [] + for t in trades: + lessons = analyze_trade(t) + for lesson_type, text, confidence in lessons: + all_lessons.append({ + 'strategy_version': 'v_next4', + 'trade_id': t.get('id', 0), + 'lesson_type': lesson_type, + 'lesson_text': text, + 'confidence': confidence, + 'profit_pct': t.get('profit_pct', 0), + 'entry_date': t.get('entry_date', ''), + }) + + # 写入 strategy_lessons 表 + for l in all_lessons: + conn.execute(""" + INSERT INTO strategy_lessons (strategy_version, trade_id, lesson_type, lesson_text, confidence, applied) + VALUES (?, ?, ?, ?, ?, 0) + """, (l['strategy_version'], l['trade_id'], l['lesson_type'], l['lesson_text'], l['confidence'])) + conn.commit() + + # 打印 + print(f"分析 {len(trades)} 笔交易, 提取 {len(all_lessons)} 条教训", flush=True) + for l in all_lessons[:5]: + print(f" [{l['lesson_type']}] {l['lesson_text']} (置信度{l['confidence']})", flush=True) + + conn.close() + return all_lessons + + +if __name__ == '__main__': + extract_lessons() diff --git a/server.py b/server.py index 634d7089..b706aaae 100644 --- a/server.py +++ b/server.py @@ -1799,6 +1799,30 @@ def api_prd(): register_routes(app) +@app.route("/api/evolution/dashboard") +def api_evolution_dashboard(): + """进化模块 Dashboard""" + try: + sys.path.insert(0, '/home/hmo/MoFin/evolution') + from evolution_api import get_evolution_dashboard + return jsonify(get_evolution_dashboard()) + except Exception as e: + return jsonify({'error': str(e)}), 500 + + +@app.route("/api/evolution/health_trend") +def api_evolution_health_trend(): + """健康度趋势""" + try: + version = request.args.get('version', 'v_next4') + days = int(request.args.get('days', 30)) + sys.path.insert(0, '/home/hmo/MoFin/evolution') + from evolution_api import get_health_trend + return jsonify({'trend': get_health_trend(version, days)}) + except Exception as e: + return jsonify({'error': str(e)}), 500 + + if __name__ == "__main__": port = int(os.environ.get("PORT", 8899)) print(f"🚀 MoFin Dashboard → http://0.0.0.0:{port}") diff --git a/static/index.html b/static/index.html index dc8781cd..9d9b7ea0 100644 --- a/static/index.html +++ b/static/index.html @@ -1929,7 +1929,8 @@ function inMd(t) { function renderResearch() { const el = document.getElementById('tab-research'); if (!el) return; - el.innerHTML = '
' + + el.innerHTML = '
🧬 进化模块加载中...
' + + '
' + '

📋 策略研究 — 版本迭代对比

' + '