feat: 自我进化模块——health_monitor+lesson_extractor+evolution_api+Dashboard+auto_iterator
This commit is contained in:
@@ -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("进化模块数据表初始化完成")
|
||||||
@@ -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()
|
||||||
@@ -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())}")
|
||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
@@ -1799,6 +1799,30 @@ def api_prd():
|
|||||||
register_routes(app)
|
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__":
|
if __name__ == "__main__":
|
||||||
port = int(os.environ.get("PORT", 8899))
|
port = int(os.environ.get("PORT", 8899))
|
||||||
print(f"🚀 MoFin Dashboard → http://0.0.0.0:{port}")
|
print(f"🚀 MoFin Dashboard → http://0.0.0.0:{port}")
|
||||||
|
|||||||
+30
-1
@@ -1929,7 +1929,8 @@ function inMd(t) {
|
|||||||
function renderResearch() {
|
function renderResearch() {
|
||||||
const el = document.getElementById('tab-research');
|
const el = document.getElementById('tab-research');
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
el.innerHTML = '<div class="flex gap-2 items-center mb-3 flex-wrap">' +
|
el.innerHTML = '<div id="evo-box" class="mb-3 bg-slate-800/60 border border-slate-700 rounded-lg p-3"><div class="text-xs text-slate-500">🧬 进化模块加载中...</div></div>' +
|
||||||
|
'<div class="flex gap-2 items-center mb-3 flex-wrap">' +
|
||||||
'<h2 class="text-lg font-bold">📋 策略研究 — 版本迭代对比</h2>' +
|
'<h2 class="text-lg font-bold">📋 策略研究 — 版本迭代对比</h2>' +
|
||||||
'<select id="btPeriod" onchange="saveBtPeriod()" class="bg-slate-800 text-sm rounded-lg px-2 py-1 border border-slate-700">' +
|
'<select id="btPeriod" onchange="saveBtPeriod()" class="bg-slate-800 text-sm rounded-lg px-2 py-1 border border-slate-700">' +
|
||||||
'<option value="1m">近 1 个月</option><option value="6m">近 6 个月</option>' +
|
'<option value="1m">近 1 个月</option><option value="6m">近 6 个月</option>' +
|
||||||
@@ -1972,6 +1973,34 @@ async function loadStrategyList() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ASC_DEFAULT = new Set(['max_dd']); // 回撤类:小=好,默认升序
|
const ASC_DEFAULT = new Set(['max_dd']); // 回撤类:小=好,默认升序
|
||||||
|
async function loadEvolutionBox() {
|
||||||
|
const box = document.getElementById('evo-box');
|
||||||
|
if (!box) return;
|
||||||
|
try {
|
||||||
|
const r = await fetch('/api/evolution/dashboard');
|
||||||
|
const d = await r.json();
|
||||||
|
let h = '<div class="flex items-center justify-between mb-1"><span class="text-xs font-semibold text-emerald-400">🧬 策略自我进化</span><button onclick="loadEvolutionBox()" class="text-[10px] text-blue-400">刷新</button></div>';
|
||||||
|
if (d.baseline) {
|
||||||
|
h += '<div class="grid grid-cols-3 gap-2 mb-2">';
|
||||||
|
for (const [v, b] of Object.entries(d.baseline)) {
|
||||||
|
const clr = v === 'v_next4' ? 'text-emerald-400' : 'text-slate-300';
|
||||||
|
h += '<div class="bg-slate-900/50 rounded px-2 py-1"><span class="'+clr+' font-mono font-bold text-xs">'+v+'</span><br><span class="text-slate-500 text-[10px]">胜率'+b.win_rate+'% 收益'+b.total_return+'%</span></div>';
|
||||||
|
}
|
||||||
|
h += '</div>';
|
||||||
|
}
|
||||||
|
if (d.health && d.health.length > 0) { const hs = d.health[0]; const sc = hs.health_score;
|
||||||
|
const color = sc >= 70 ? 'text-emerald-400' : sc >= 50 ? 'text-amber-400' : 'text-red-400';
|
||||||
|
h += '<div class="text-xs '+color+'">健康度: '+sc+'分 (实盘'+hs.live_trades+'笔)</div>'; }
|
||||||
|
if (d.lessons && d.lessons.length > 0) {
|
||||||
|
h += '<div class="text-xs text-slate-500 mt-1">最近教训:</div>';
|
||||||
|
for (const l of d.lessons.slice(0,3)) h += '<div class="text-xs text-slate-400">'+(l.lesson_type==='win_pattern'?'✅':'❌')+' '+l.lesson_text+'</div>';
|
||||||
|
}
|
||||||
|
box.innerHTML = h;
|
||||||
|
} catch (e) { box.innerHTML = '<div class="text-xs text-red-400">加载失败</div>'; }
|
||||||
|
}
|
||||||
|
// 研究Tab活跃时加载进化模块
|
||||||
|
new MutationObserver(()=>{if(!document.getElementById('tab-research')?.classList?.contains('hidden'))loadEvolutionBox();}).observe(document.body,{attributes:true,subtree:true});
|
||||||
|
|
||||||
function showDesc(version) {
|
function showDesc(version) {
|
||||||
const s = (window._strats || []).find(x => x.version === version);
|
const s = (window._strats || []).find(x => x.version === version);
|
||||||
const d = s && s.description;
|
const d = s && s.description;
|
||||||
|
|||||||
Reference in New Issue
Block a user