在后端新增了全局交易统计功能,包括按交易对和按小时的聚合统计,支持生成软黑名单和时段过滤建议。前端组件更新以展示基于最近N天交易统计的过滤结果,旨在提升交易策略的灵活性和风险控制能力。
165 lines
6.5 KiB
Python
165 lines
6.5 KiB
Python
#!/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 视为表现不佳
|
||
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:
|
||
symbol_blacklist.append(
|
||
{
|
||
"symbol": sym,
|
||
"trade_count": tc,
|
||
"net_pnl": round(net_pnl, 4),
|
||
"win_rate_pct": round(win_rate, 1),
|
||
"mode": "reduced", # 软黑名单:仅降权+提门槛
|
||
}
|
||
)
|
||
# 白名单:近期净盈且胜率尚可
|
||
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()
|