#!/usr/bin/env python3 """ 定时任务:将最近 N 天的交易数据聚合到 trade_stats_daily / trade_stats_time_bucket。 优先从 binance_trades 读取(需先跑 sync_binance_orders.py),无数据时用 trades 表。 用法: python scripts/aggregate_trade_stats.py # 默认 7 天、默认账号 python scripts/aggregate_trade_stats.py -d 30 # 最近 30 天 python scripts/aggregate_trade_stats.py -a 2 # 指定账号 """ import argparse import sys from pathlib import Path proj = Path(__file__).resolve().parent.parent if (proj / "backend").exists(): sys.path.insert(0, str(proj / "backend")) sys.path.insert(0, str(proj)) def main(): parser = argparse.ArgumentParser(description="聚合交易统计到 trade_stats_* 表") parser.add_argument("-a", "--account", type=int, default=None, help="账号 ID,不传则用默认") parser.add_argument("-d", "--days", type=int, default=7, help="聚合最近 N 天,默认 7") args = parser.parse_args() if args.days <= 0: print("days 须 > 0") sys.exit(1) try: from datetime import datetime, timezone from database.models import TradeStats, GlobalStrategyConfig, TradingConfig # 1) 先按账号聚合到 trade_stats_* 表(供仪表盘等使用) TradeStats.aggregate_recent_days(days=args.days, account_id=args.account) print(f"已聚合最近 {args.days} 天统计 (account_id={args.account or 'default'})") # 2) 再基于 trade_stats_* 做一次【全局】评估:按交易对 / 按小时生成软黑名单与时段过滤建议 days = max(1, int(args.days)) symbol_rows = TradeStats.get_global_symbol_stats(days=days) hour_rows = TradeStats.get_global_hourly_stats(days=days) # 规则参数(后续可改成全局配置,这里先写死一版直观规则): min_trades_for_symbol = 5 # 至少 N 笔才纳入评估,避免样本太少 min_trades_for_hour = 10 # 小时至少 N 笔才纳入评估 blacklist_pnl_threshold = 0.0 # 净盈亏 < 0 视为表现不佳(软黑名单) hard_blacklist_pnl_threshold = -3.0 # 净亏绝对值较大时考虑升级为硬黑名单 hard_blacklist_min_trades = 10 # 硬黑名单至少需要的样本数 hard_blacklist_max_win_rate = 10.0 # 胜率≤10% 且净亏较大时视为严重拖累 good_hour_pnl_threshold = 0.0 # 净盈亏 > 0 视为表现较好 symbol_blacklist = [] symbol_whitelist = [] for r in symbol_rows or []: try: sym = (r.get("symbol") or "").strip() tc = int(r.get("trade_count") or 0) net_pnl = float(r.get("net_pnl") or 0.0) win = int(r.get("win_count") or 0) loss = int(r.get("loss_count") or 0) except Exception: continue if not sym or tc < min_trades_for_symbol: continue total = max(1, win + loss) win_rate = 100.0 * win / total # 黑名单:近期净亏 + 有一定笔数 -> 降权或升级为硬黑名单 if net_pnl < blacklist_pnl_threshold: mode = "reduced" if ( net_pnl <= hard_blacklist_pnl_threshold and tc >= hard_blacklist_min_trades and win_rate <= hard_blacklist_max_win_rate ): mode = "hard" symbol_blacklist.append( { "symbol": sym, "trade_count": tc, "net_pnl": round(net_pnl, 4), "win_rate_pct": round(win_rate, 1), "mode": mode, # reduced=软黑名单(降权+提门槛);hard=硬黑名单(自动不再开仓) } ) # 白名单:近期净盈且胜率尚可 elif net_pnl > 0 and win_rate >= 50.0: symbol_whitelist.append( { "symbol": sym, "trade_count": tc, "net_pnl": round(net_pnl, 4), "win_rate_pct": round(win_rate, 1), } ) hour_filters = [] for r in hour_rows or []: try: h = int(r.get("hour")) tc = int(r.get("trade_count") or 0) net_pnl = float(r.get("net_pnl") or 0.0) except Exception: continue if h < 0 or h > 23: continue if tc < min_trades_for_hour: bucket = "unknown" elif net_pnl > good_hour_pnl_threshold: bucket = "good" elif net_pnl < -good_hour_pnl_threshold: bucket = "bad" else: bucket = "neutral" # 约定:bad 时段默认 0.5 仓位系数 + 信号门槛+1;good 时段略微放宽(可视需求调整) if bucket == "bad": position_factor = 0.5 signal_boost = 1 elif bucket == "good": position_factor = 1.1 signal_boost = 0 else: position_factor = 1.0 signal_boost = 0 hour_filters.append( { "hour": h, "bucket": bucket, "trade_count": tc, "net_pnl": round(net_pnl, 4), "position_factor": position_factor, "signal_boost": signal_boost, } ) now_iso = datetime.now(timezone.utc).isoformat() symbol_payload = { "generated_at": now_iso, "days": days, "min_trades": min_trades_for_symbol, "blacklist": sorted(symbol_blacklist, key=lambda x: (x["net_pnl"], -x["trade_count"])), "whitelist": sorted(symbol_whitelist, key=lambda x: (-x["net_pnl"], -x["trade_count"])), } hour_payload = { "generated_at": now_iso, "days": days, "min_trades": min_trades_for_hour, "hours": sorted(hour_filters, key=lambda x: x["hour"]), } # 写入全局策略配置(json),供交易进程 & 前端全局配置页读取 GlobalStrategyConfig.set( "STATS_SYMBOL_FILTERS", symbol_payload, "json", "stats_filters", description="基于最近 N 天交易统计自动生成的 symbol 白/黑名单(软降权),请勿手动修改。", ) GlobalStrategyConfig.set( "STATS_HOUR_FILTERS", hour_payload, "json", "stats_filters", description="基于最近 N 天交易统计自动生成的按小时时段过滤建议,请勿手动修改。", ) print(f"已更新全局统计过滤配置:{len(symbol_blacklist)} 个黑名单 symbol,{len(symbol_whitelist)} 个白名单 symbol。") except Exception as e: print(f"聚合失败: {e}") sys.exit(1) if __name__ == "__main__": main()