feat(stats): 增加全局交易统计功能
在后端新增了全局交易统计功能,包括按交易对和按小时的聚合统计,支持生成软黑名单和时段过滤建议。前端组件更新以展示基于最近N天交易统计的过滤结果,旨在提升交易策略的灵活性和风险控制能力。
This commit is contained in:
parent
076b597fb4
commit
861f1dc548
|
|
@ -1527,6 +1527,60 @@ class TradeStats:
|
|||
logger.debug(f"[TradeStats] get_hourly_stats 失败: {e}")
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def get_global_symbol_stats(days: int = 7):
|
||||
"""
|
||||
查询最近 N 天【跨账号聚合】的按交易对统计(无 account_id 维度),
|
||||
用于全局白名单/黑名单评估。「大家一视同仁」。
|
||||
"""
|
||||
try:
|
||||
rows = db.execute_query(
|
||||
"""SELECT
|
||||
symbol,
|
||||
SUM(trade_count) AS trade_count,
|
||||
SUM(win_count) AS win_count,
|
||||
SUM(loss_count) AS loss_count,
|
||||
SUM(gross_pnl) AS gross_pnl,
|
||||
SUM(net_pnl) AS net_pnl,
|
||||
SUM(total_commission) AS total_commission
|
||||
FROM trade_stats_daily
|
||||
WHERE trade_date >= DATE_SUB(CURDATE(), INTERVAL %s DAY)
|
||||
GROUP BY symbol
|
||||
ORDER BY net_pnl DESC""",
|
||||
(max(1, int(days)),),
|
||||
)
|
||||
return [dict(r) for r in rows] if rows else []
|
||||
except Exception as e:
|
||||
logger.debug(f"[TradeStats] get_global_symbol_stats 失败: {e}")
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def get_global_hourly_stats(days: int = 7):
|
||||
"""
|
||||
查询最近 N 天【跨账号聚合】的按小时统计(无 account_id 维度),
|
||||
用于全局时段表现评估。
|
||||
"""
|
||||
try:
|
||||
rows = db.execute_query(
|
||||
"""SELECT
|
||||
hour,
|
||||
SUM(trade_count) AS trade_count,
|
||||
SUM(win_count) AS win_count,
|
||||
SUM(loss_count) AS loss_count,
|
||||
SUM(gross_pnl) AS gross_pnl,
|
||||
SUM(net_pnl) AS net_pnl,
|
||||
SUM(total_commission) AS total_commission
|
||||
FROM trade_stats_time_bucket
|
||||
WHERE trade_date >= DATE_SUB(CURDATE(), INTERVAL %s DAY)
|
||||
GROUP BY hour
|
||||
ORDER BY hour ASC""",
|
||||
(max(1, int(days)),),
|
||||
)
|
||||
return [dict(r) for r in rows] if rows else []
|
||||
except Exception as e:
|
||||
logger.debug(f"[TradeStats] get_global_hourly_stats 失败: {e}")
|
||||
return []
|
||||
|
||||
|
||||
class AccountSnapshot:
|
||||
"""账户快照模型"""
|
||||
|
|
|
|||
|
|
@ -568,6 +568,9 @@ const GlobalConfig = () => {
|
|||
BLOCK_LONG_WHEN_4H_DOWN: { value: false, type: 'boolean', category: 'strategy', description: '4H 趋势下跌时禁止开多。bear / conservative 方案下自动为 true。' },
|
||||
BLOCK_SHORT_WHEN_4H_UP: { value: true, type: 'boolean', category: 'strategy', description: '4H 趋势上涨时禁止开空。默认 true,避免逆势做空导致止损。' },
|
||||
AUTO_MARKET_SCHEME_ENABLED: { value: false, type: 'boolean', category: 'strategy', description: '开启后,crontab 定时运行 update_market_scheme.py --apply 时自动更新 MARKET_SCHEME(根据 BTC 行情识别牛/熊/正常)。' },
|
||||
// 基于7天统计自动生成的全局过滤结果(仅展示,自动更新)
|
||||
STATS_SYMBOL_FILTERS: { value: {}, type: 'json', category: 'stats', description: '基于最近 N 天交易统计自动生成的 symbol 白/黑名单(软降权),由脚本 aggregate_trade_stats.py 自动更新,请勿手动修改。' },
|
||||
STATS_HOUR_FILTERS: { value: {}, type: 'json', category: 'stats', description: '基于最近 N 天交易统计自动生成的按小时过滤建议,用于差时段减仓/提门槛,由脚本 aggregate_trade_stats.py 自动更新,请勿手动修改。' },
|
||||
}
|
||||
|
||||
const loadConfigs = async () => {
|
||||
|
|
|
|||
|
|
@ -27,9 +27,134 @@ def main():
|
|||
print("days 须 > 0")
|
||||
sys.exit(1)
|
||||
try:
|
||||
from database.models import TradeStats
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -771,7 +771,7 @@ class RiskManager:
|
|||
# 计算最终的名义价值与保证金
|
||||
final_notional_value = quantity * current_price
|
||||
final_margin = final_notional_value / actual_leverage if actual_leverage > 0 else final_notional_value
|
||||
|
||||
|
||||
# 仓位放大系数(盈利时适当放大仓位,1.0=正常,1.2=+20%,上限2.0,仍受 MAX_POSITION_PERCENT 约束)
|
||||
position_scale = config.TRADING_CONFIG.get('POSITION_SCALE_FACTOR', 1.0)
|
||||
try:
|
||||
|
|
@ -779,8 +779,47 @@ class RiskManager:
|
|||
position_scale = max(1.0, min(position_scale, 2.0))
|
||||
except (TypeError, ValueError):
|
||||
position_scale = 1.0
|
||||
if position_scale > 1.0:
|
||||
quantity = quantity * position_scale
|
||||
|
||||
# 基于 7 天统计的 symbol / 小时软黑名单:差标的 & 差时段进一步缩小仓位
|
||||
stats_scale = 1.0
|
||||
try:
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from config_manager import GlobalStrategyConfigManager
|
||||
|
||||
mgr = GlobalStrategyConfigManager()
|
||||
stats_symbol = mgr.get("STATS_SYMBOL_FILTERS", {}) or {}
|
||||
stats_hour = mgr.get("STATS_HOUR_FILTERS", {}) or {}
|
||||
|
||||
bl = stats_symbol.get("blacklist") or []
|
||||
if symbol and isinstance(bl, list):
|
||||
if any((item.get("symbol") or "").upper() == symbol.upper() for item in bl):
|
||||
# 软黑名单:默认 0.5 倍仓位
|
||||
stats_scale *= 0.5
|
||||
|
||||
# 小时过滤
|
||||
try:
|
||||
bj = timezone(timedelta(hours=8))
|
||||
h = datetime.now(bj).hour
|
||||
hours = stats_hour.get("hours") or []
|
||||
for item in hours:
|
||||
if int(item.get("hour", -1)) == int(h):
|
||||
pf = item.get("position_factor")
|
||||
try:
|
||||
pf_val = float(pf)
|
||||
except (TypeError, ValueError):
|
||||
pf_val = 1.0
|
||||
# 仅在 <1 时缩小仓位;>1 的“好时段”放大留给 POSITION_SCALE_FACTOR 处理
|
||||
if pf_val > 0 and pf_val < 1.0:
|
||||
stats_scale *= pf_val
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
stats_scale = 1.0
|
||||
|
||||
total_scale = position_scale * stats_scale
|
||||
if total_scale != 1.0:
|
||||
quantity = quantity * total_scale
|
||||
final_notional_value = quantity * current_price
|
||||
final_margin = final_notional_value / actual_leverage if actual_leverage > 0 else final_notional_value
|
||||
max_margin_cap = available_balance * config.get_effective_config('MAX_POSITION_PERCENT', 0.20)
|
||||
|
|
@ -789,7 +828,7 @@ class RiskManager:
|
|||
final_notional_value = final_margin * actual_leverage
|
||||
quantity = final_notional_value / current_price if current_price and current_price > 0 else quantity
|
||||
logger.info(f" 仓位放大后超过单笔上限,已截断至 MAX_POSITION_PERCENT 对应保证金")
|
||||
logger.info(f" 仓位放大系数: {position_scale:.2f} -> 最终数量: {quantity:.4f}")
|
||||
logger.info(f" 仓位缩放系数: POSITION_SCALE={position_scale:.2f}, STATS_SCALE={stats_scale:.2f} -> 最终数量: {quantity:.4f}")
|
||||
|
||||
# 添加最小名义价值检查(0.2 USDT),避免下无意义的小单子
|
||||
MIN_NOTIONAL_VALUE = 0.2 # 最小名义价值0.2 USDT
|
||||
|
|
|
|||
|
|
@ -688,7 +688,41 @@ class TradingStrategy:
|
|||
reasons.append("❌ 逆4H趋势,拒绝交易")
|
||||
|
||||
# 判断是否应该交易(信号强度 >= 有效 MIN_SIGNAL_STRENGTH 才交易;低波动期自动提高门槛)
|
||||
min_signal_strength = config.get_effective_config('MIN_SIGNAL_STRENGTH', 7)
|
||||
base_min_signal_strength = config.get_effective_config('MIN_SIGNAL_STRENGTH', 7)
|
||||
|
||||
# 基于全局 7 天统计的 symbol / 小时过滤:差标的、差时段提高信号门槛
|
||||
extra_signal_boost = 0
|
||||
try:
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from config_manager import GlobalStrategyConfigManager
|
||||
|
||||
mgr = GlobalStrategyConfigManager()
|
||||
stats_symbol = mgr.get("STATS_SYMBOL_FILTERS", {}) or {}
|
||||
stats_hour = mgr.get("STATS_HOUR_FILTERS", {}) or {}
|
||||
|
||||
# symbol 软黑名单:命中则在基础门槛上 +1
|
||||
bl = stats_symbol.get("blacklist") or []
|
||||
if symbol and isinstance(bl, list):
|
||||
if any((item.get("symbol") or "").upper() == symbol.upper() for item in bl):
|
||||
extra_signal_boost += 1
|
||||
|
||||
# 按小时过滤:bad 小时再 +1
|
||||
try:
|
||||
bj = timezone(timedelta(hours=8))
|
||||
h = datetime.now(bj).hour
|
||||
hours = stats_hour.get("hours") or []
|
||||
for item in hours:
|
||||
if int(item.get("hour", -1)) == int(h):
|
||||
if (item.get("bucket") or "").lower() == "bad":
|
||||
extra_signal_boost += int(item.get("signal_boost") or 1)
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
base_min_signal_strength = config.get_effective_config('MIN_SIGNAL_STRENGTH', 7)
|
||||
extra_signal_boost = 0
|
||||
|
||||
min_signal_strength = max(0, min(10, int(base_min_signal_strength) + int(extra_signal_boost)))
|
||||
# 强度上限归一到 0-10,避免出现 12/10 这种误导显示
|
||||
try:
|
||||
signal_strength = max(0, min(int(signal_strength), 10))
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user