""" 山寨币策略回测脚本 对 DB 中所有有完整入场方案(stop_loss/tp1/tp2)的推荐做模拟跟踪。 """ import sys, os, json, sqlite3 from datetime import datetime, timedelta sys.path.insert(0, '/home/ubuntu/quant_monitor/altcoin') import ccxt import pandas as pd import numpy as np exchange = ccxt.binance({'enableRateLimit': True}) DB = '/home/ubuntu/quant_monitor/altcoin/altcoin_monitor.db' def fetch_klines_since(symbol, timeframe, since_ms, limit=500): """Fetch K-lines from a specific timestamp.""" try: ohlcv = exchange.fetch_ohlcv(symbol, timeframe, since=since_ms, limit=limit) if not ohlcv: return None df = pd.DataFrame(ohlcv, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume']) df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms') return df except Exception as e: print(f" fetch_klines error for {symbol}: {e}") return None def simulate_trade(rec, klines_df): """Simulate one trade: walk through K-lines, check TP/stop.""" entry_price = float(rec['entry_price']) stop_loss = float(rec['stop_loss'] or 0) tp1 = float(rec['tp1'] or 0) tp2 = float(rec['tp2'] or 0) if stop_loss <= 0 or tp1 <= 0: return {'result': 'no_entry_plan', 'exit_price': 0, 'exit_time': '', 'pnl_pct': 0, 'hours': 0} result = 'expired' exit_price = entry_price exit_time = '' max_profit_pct = 0 max_loss_pct = 0 for _, row in klines_df.iterrows(): high = float(row['high']) low = float(row['low']) close = float(row['close']) ts = row['timestamp'] current_pnl = (close / entry_price - 1) * 100 max_profit_pct = max(max_profit_pct, (high / entry_price - 1) * 100) max_loss_pct = min(max_loss_pct, (low / entry_price - 1) * 100) # Check TP2 first (higher target) if tp2 > 0 and high >= tp2: result = 'hit_tp2' exit_price = tp2 exit_time = str(ts) break # Check TP1 if tp1 > 0 and high >= tp1: result = 'hit_tp1' exit_price = tp1 exit_time = str(ts) break # Check stop loss if stop_loss > 0 and low <= stop_loss: result = 'stopped_out' exit_price = stop_loss exit_time = str(ts) break pnl_pct = round((exit_price / entry_price - 1) * 100, 2) # Calculate holding hours if exit_time: try: et = datetime.fromisoformat(exit_time) rt = datetime.fromisoformat(rec['rec_time']) hours = round((et - rt).total_seconds() / 3600, 1) except: hours = 0 else: hours = 0 return { 'result': result, 'exit_price': round(exit_price, 6), 'exit_time': exit_time, 'pnl_pct': pnl_pct, 'max_profit_pct': round(max_profit_pct, 2), 'max_loss_pct': round(max_loss_pct, 2), 'hours': hours, } def main(): conn = sqlite3.connect(DB) conn.row_factory = sqlite3.Row # Only backtest "爆发" with full entry plans rows = conn.execute(""" SELECT id, symbol, rec_time, rec_state, rec_score, entry_price, stop_loss, tp1, tp2, status, entry_plan_json, signals, sector FROM recommendation WHERE stop_loss > 0 AND tp1 > 0 ORDER BY id """).fetchall() conn.close() print(f"回测样本: {len(rows)} 条 (有完整入场方案)\n") results = [] wins = losses = expired_count = 0 total_pnl = 0 max_win = -999 max_loss = 999 for i, rec in enumerate(rows, 1): symbol = rec['symbol'] rec_time = datetime.fromisoformat(rec['rec_time']) since_ms = int(rec_time.timestamp() * 1000) print(f"[{i}/{len(rows)}] {symbol} rec_time={rec['rec_time'][:19]} " f"entry={rec['entry_price']} stop={rec['stop_loss']} tp1={rec['tp1']} tp2={rec['tp2']}", end='') klines = fetch_klines_since(symbol, '15m', since_ms, limit=2000) if klines is None or len(klines) < 2: print(" → 数据不足,跳过") continue sim = simulate_trade(rec, klines) results.append({**sim, 'symbol': symbol, 'rec_time': rec['rec_time'], 'entry_price': rec['entry_price'], 'rec_state': rec['rec_state']}) tag = {'hit_tp1': '🟢', 'hit_tp2': '🟢🟢', 'stopped_out': '🔴', 'expired': '⏰', 'no_entry_plan': '❓'} print(f" → {tag.get(sim['result'], '?')} {sim['result']} pnl={sim['pnl_pct']}% " f"max_profit={sim['max_profit_pct']}% max_loss={sim['max_loss_pct']}% {sim['hours']}h") if sim['result'] in ('hit_tp1', 'hit_tp2'): wins += 1 total_pnl += sim['pnl_pct'] max_win = max(max_win, sim['pnl_pct']) elif sim['result'] == 'stopped_out': losses += 1 total_pnl += sim['pnl_pct'] max_loss = min(max_loss, sim['pnl_pct']) else: expired_count += 1 print(f"\n{'='*60}") print(f"回测汇总 (n={len(results)})") print(f"{'='*60}") print(f"止盈(TP): {wins} 笔") print(f"止损: {losses} 笔") print(f"未触达: {expired_count} 笔") closed = wins + losses if closed > 0: print(f"胜率: {wins}/{closed} = {round(wins/closed*100,1)}%") print(f"平均盈亏: {round(total_pnl/closed, 2)}%") print(f"最大盈利: {max_win}%") print(f"最大亏损: {max_loss}%") print(f"盈亏比: {round(abs(max_win/max_loss) if max_loss != 0 else 99, 1)}") print(f"{'='*60}") # Detail table print(f"\n{'symbol':<14} {'time':<17} {'result':<14} {'pnl':>7} {'max+':>7} {'max-':>7} {'h':>5}") print("-" * 70) for r in results: print(f"{r['symbol']:<14} {r['rec_time'][:16]:<17} {r['result']:<14} " f"{r['pnl_pct']:>6.1f}% {r['max_profit_pct']:>6.1f}% {r['max_loss_pct']:>6.1f}% {r['hours']:>5.1f}") # Save to JSON for HTML report with open('/home/ubuntu/quant_monitor/altcoin/backtest_result.json', 'w') as f: json.dump({ 'generated_at': datetime.now().isoformat(), 'total': len(results), 'wins': wins, 'losses': losses, 'expired': expired_count, 'win_rate': round(wins/closed*100, 1) if closed > 0 else 0, 'avg_pnl': round(total_pnl/closed, 2) if closed > 0 else 0, 'max_win': max_win, 'max_loss': max_loss, 'details': [{k: str(v) if isinstance(v, (datetime, pd.Timestamp)) else v for k, v in r.items()} for r in results], }, f, ensure_ascii=False, indent=2, default=str) print(f"\n结果已保存: backtest_result.json") if __name__ == '__main__': main()