11
This commit is contained in:
parent
89250046c3
commit
fca8a961ba
@ -80,6 +80,8 @@ ALPHAX_PAPER_GLOBAL_RISK_HIGH_MIN_REC_SCORE=70
|
||||
ALPHAX_PAPER_GLOBAL_RISK_HIGH_DRAWDOWN_PCT=3
|
||||
ALPHAX_PAPER_GLOBAL_RISK_CRITICAL_DRAWDOWN_PCT=6
|
||||
ALPHAX_PAPER_GLOBAL_RISK_MAX_OPEN_POSITIONS=0
|
||||
ALPHAX_PAPER_GLOBAL_RISK_MAX_SAME_SECTOR_POSITIONS=3
|
||||
ALPHAX_PAPER_GLOBAL_RISK_MAX_SAME_DIRECTION_POSITIONS=6
|
||||
ALPHAX_PAPER_ORDER_MIN_REC_SCORE=50
|
||||
ALPHAX_PAPER_ORDER_MIN_RR=1.8
|
||||
ALPHAX_PAPER_ORDER_REQUIRE_RISK_REWARD_OK=1
|
||||
|
||||
@ -132,6 +132,8 @@ def default_paper_trading_config():
|
||||
"global_risk_high_drawdown_pct": _env_float("ALPHAX_PAPER_GLOBAL_RISK_HIGH_DRAWDOWN_PCT", 3.0),
|
||||
"global_risk_critical_drawdown_pct": _env_float("ALPHAX_PAPER_GLOBAL_RISK_CRITICAL_DRAWDOWN_PCT", 6.0),
|
||||
"global_risk_max_open_positions": _env_int("ALPHAX_PAPER_GLOBAL_RISK_MAX_OPEN_POSITIONS", 0),
|
||||
"global_risk_max_same_sector_positions": _env_int("ALPHAX_PAPER_GLOBAL_RISK_MAX_SAME_SECTOR_POSITIONS", 3),
|
||||
"global_risk_max_same_direction_positions": _env_int("ALPHAX_PAPER_GLOBAL_RISK_MAX_SAME_DIRECTION_POSITIONS", 6),
|
||||
"order_min_rec_score": _env_float("ALPHAX_PAPER_ORDER_MIN_REC_SCORE", 50.0),
|
||||
"order_min_rr": _env_float("ALPHAX_PAPER_ORDER_MIN_RR", 1.8),
|
||||
"order_require_risk_reward_ok": _env_bool("ALPHAX_PAPER_ORDER_REQUIRE_RISK_REWARD_OK", True),
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from app.core.market_regime import classify_market_regime
|
||||
from app.core.sector_map import get_sector_for_coin
|
||||
from app.services.market_overview import get_crypto_market_overview
|
||||
|
||||
|
||||
@ -46,6 +47,48 @@ def _portfolio_snapshot(conn, account_equity: float, additional_notional: float)
|
||||
}
|
||||
|
||||
|
||||
def _sector_names(symbol: str, rec: dict | None = None) -> list[str]:
|
||||
sectors = []
|
||||
rec = rec or {}
|
||||
raw = rec.get("sector") or ""
|
||||
if raw:
|
||||
sectors.extend([x.strip() for x in str(raw).split(",") if x.strip()])
|
||||
if not sectors:
|
||||
sectors.extend(get_sector_for_coin(symbol))
|
||||
return sorted({x for x in sectors if x})
|
||||
|
||||
|
||||
def _concentration_snapshot(conn, rec: dict | None = None) -> dict:
|
||||
rec = rec or {}
|
||||
target_symbol = str(rec.get("symbol") or "").strip().upper()
|
||||
target_side = str(rec.get("side") or rec.get("direction") or "long").strip().lower()
|
||||
target_sectors = _sector_names(target_symbol, rec)
|
||||
open_rows = [dict(r) for r in conn.execute("SELECT symbol, side, notional_usdt FROM paper_trades WHERE status='open'").fetchall()]
|
||||
same_direction_count = 0
|
||||
same_direction_notional = 0.0
|
||||
sector_counts = {sector: 0 for sector in target_sectors}
|
||||
sector_notional = {sector: 0.0 for sector in target_sectors}
|
||||
for row in open_rows:
|
||||
row_side = str(row.get("side") or "long").strip().lower()
|
||||
if row_side == target_side:
|
||||
same_direction_count += 1
|
||||
same_direction_notional += _safe_float(row.get("notional_usdt"))
|
||||
row_sectors = _sector_names(row.get("symbol") or "")
|
||||
for sector in target_sectors:
|
||||
if sector in row_sectors:
|
||||
sector_counts[sector] = sector_counts.get(sector, 0) + 1
|
||||
sector_notional[sector] = sector_notional.get(sector, 0.0) + _safe_float(row.get("notional_usdt"))
|
||||
return {
|
||||
"target_symbol": target_symbol,
|
||||
"target_side": target_side,
|
||||
"target_sectors": target_sectors,
|
||||
"same_direction_count": same_direction_count,
|
||||
"same_direction_notional_usdt": round(same_direction_notional, 8),
|
||||
"same_sector_counts": sector_counts,
|
||||
"same_sector_notional_usdt": {k: round(v, 8) for k, v in sector_notional.items()},
|
||||
}
|
||||
|
||||
|
||||
def evaluate_global_risk(
|
||||
*,
|
||||
conn,
|
||||
@ -69,6 +112,7 @@ def evaluate_global_risk(
|
||||
regime = classify_market_regime(overview)
|
||||
account_equity = max(1.0, _safe_float(cfg.get("account_equity_usdt"), 20000.0))
|
||||
portfolio = _portfolio_snapshot(conn, account_equity, additional_notional)
|
||||
concentration = _concentration_snapshot(conn, rec)
|
||||
rec_score = _safe_float((rec or {}).get("rec_score") or (rec or {}).get("score"))
|
||||
min_score_high = max(0.0, _safe_float(cfg.get("global_risk_high_min_rec_score"), 70.0))
|
||||
max_drawdown_critical = max(0.0, _safe_float(cfg.get("global_risk_critical_drawdown_pct"), 6.0))
|
||||
@ -101,6 +145,25 @@ def evaluate_global_risk(
|
||||
risk_level = "high" if risk_level not in {"critical"} else risk_level
|
||||
reasons.append(f"持仓数量已达到上限 {max_open_positions}")
|
||||
|
||||
max_same_direction = max(0, _safe_int(cfg.get("global_risk_max_same_direction_positions"), 0))
|
||||
projected_same_direction = _safe_int(concentration.get("same_direction_count")) + (1 if rec else 0)
|
||||
if allow and max_same_direction > 0 and projected_same_direction > max_same_direction:
|
||||
allow = False
|
||||
decision = "block_same_direction_concentration"
|
||||
risk_level = "high" if risk_level not in {"critical"} else risk_level
|
||||
reasons.append(f"同方向持仓将达到 {projected_same_direction} 个,超过上限 {max_same_direction}")
|
||||
|
||||
max_same_sector = max(0, _safe_int(cfg.get("global_risk_max_same_sector_positions"), 0))
|
||||
if allow and max_same_sector > 0:
|
||||
for sector, count in (concentration.get("same_sector_counts") or {}).items():
|
||||
projected = _safe_int(count) + 1
|
||||
if projected > max_same_sector:
|
||||
allow = False
|
||||
decision = "block_same_sector_concentration"
|
||||
risk_level = "high" if risk_level not in {"critical"} else risk_level
|
||||
reasons.append(f"{sector} 板块持仓将达到 {projected} 个,超过上限 {max_same_sector}")
|
||||
break
|
||||
|
||||
return {
|
||||
"enabled": True,
|
||||
"allow_new_entries": allow,
|
||||
@ -112,6 +175,7 @@ def evaluate_global_risk(
|
||||
"reasons": reasons,
|
||||
"market_regime": regime,
|
||||
"portfolio": portfolio,
|
||||
"concentration": concentration,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -25,6 +25,8 @@ DEFAULT_ENTRY_GATE = {
|
||||
"low_plan_min_static_count": 3,
|
||||
"low_plan_min_top_long_pct": 55,
|
||||
"max_wait_pullback_deviation_pct": 12,
|
||||
"min_entry_score_buy_now": 3,
|
||||
"min_entry_score_wait_pullback": 1,
|
||||
}
|
||||
|
||||
|
||||
@ -311,6 +313,9 @@ def apply_entry_quality_gate(
|
||||
invalid_plan_geometry = True
|
||||
reasons.append("多头计划无效:TP1不高于计划入场价,转为观察")
|
||||
entry_action = str(entry_plan.get("entry_action") or "").strip()
|
||||
score_components = normalize_json_object(entry_plan.get("score_components"))
|
||||
has_entry_score = "entry_score" in score_components
|
||||
entry_score = to_float(score_components.get("entry_score")) if has_entry_score else None
|
||||
opportunity_level = str(entry_plan.get("opportunity_level") or "").strip()
|
||||
level_meta = OPPORTUNITY_LEVELS.get(opportunity_level, {})
|
||||
level_max_action = level_meta.get("max_action") or str(entry_plan.get("max_action") or "").strip()
|
||||
@ -346,6 +351,8 @@ def apply_entry_quality_gate(
|
||||
reasons.append(f"rr1={rr1} < {_cfg_value(cfg, 'min_rr_buy_now')},禁止现价买入")
|
||||
|
||||
if action_status == "可即刻买入":
|
||||
if has_entry_score and entry_score < _cfg_value(cfg, "min_entry_score_buy_now"):
|
||||
reasons.append(f"买点分{entry_score:.1f}低于现价买入门槛{_cfg_value(cfg, 'min_entry_score_buy_now')},禁止立即买入")
|
||||
if level_max_action == "observe":
|
||||
reasons.append(f"{entry_plan.get('opportunity_level_label') or opportunity_level or '该机会'}级别最高只允许{level_meta.get('label') and ('观察/等待') or '观察/等待'},禁止现价买入")
|
||||
elif level_max_action == "wait_pullback" and not current_entry_trigger:
|
||||
@ -369,6 +376,9 @@ def apply_entry_quality_gate(
|
||||
reasons.append(f"24h涨幅{change_24h:.1f}%且rr1不足,禁止追涨")
|
||||
|
||||
if action_status == "等回踩" and current_price > 0:
|
||||
if has_entry_score and entry_score < _cfg_value(cfg, "min_entry_score_wait_pullback"):
|
||||
target_action = "观察"
|
||||
reasons.append(f"买点分{entry_score:.1f}低于挂单门槛{_cfg_value(cfg, 'min_entry_score_wait_pullback')},不生成回踩挂单")
|
||||
if plan_entry_price > 0:
|
||||
wait_deviation_pct = round((current_price / plan_entry_price - 1) * 100, 2)
|
||||
entry_plan["wait_pullback_deviation_pct"] = wait_deviation_pct
|
||||
@ -419,6 +429,9 @@ def apply_entry_quality_gate(
|
||||
elif action_status == "等回踩" and current_price > 0 and to_float(entry_plan.get("entry_price")) > 0 and current_price <= to_float(entry_plan.get("entry_price")) * 1.003 and (risk_reward_ok is False or rr1 < _cfg_value(cfg, "min_rr_buy_now")):
|
||||
target_action = "观察"
|
||||
reasons.append("回踩参考已到,但实时盈亏比不达标,转为观察")
|
||||
elif has_entry_score and action_status == "可即刻买入" and entry_score < _cfg_value(cfg, "min_entry_score_wait_pullback"):
|
||||
target_action = "观察"
|
||||
reasons.append("买点分不足以进入挂单池,转为观察")
|
||||
elif action_status == "可即刻买入" and current_price > 0 and stop_loss > 0 and tp1 > stop_loss and (risk_reward_ok is False or rr1 < _cfg_value(cfg, "min_rr_buy_now")):
|
||||
rr_target_entry = calc_rr_target_entry(stop_loss, tp1, _cfg_value(cfg, "min_rr_buy_now"))
|
||||
if rr_target_entry > stop_loss and rr_target_entry < current_price:
|
||||
@ -456,4 +469,8 @@ def apply_entry_quality_gate(
|
||||
"breakout_distance_pct": breakout_distance,
|
||||
"change_24h": change_24h,
|
||||
}
|
||||
if has_entry_score:
|
||||
entry_plan["entry_quality_gate"]["entry_score"] = entry_score
|
||||
entry_plan["entry_quality_gate"]["min_entry_score_buy_now"] = _cfg_value(cfg, "min_entry_score_buy_now")
|
||||
entry_plan["entry_quality_gate"]["min_entry_score_wait_pullback"] = _cfg_value(cfg, "min_entry_score_wait_pullback")
|
||||
return target_action, entry_plan, reasons
|
||||
|
||||
@ -141,6 +141,7 @@ def _opportunity_review(conn, since):
|
||||
def _paper_review(conn, since, days):
|
||||
summary = get_paper_trading_summary(days=days)
|
||||
trade_attribution = (get_strategy_insights().get("trade_attribution") or {})
|
||||
watch_order_attribution = (get_strategy_insights().get("watch_order_attribution") or {})
|
||||
trades = [dict(r) for r in conn.execute(
|
||||
"""
|
||||
SELECT *
|
||||
@ -169,6 +170,7 @@ def _paper_review(conn, since, days):
|
||||
"exit_reasons": exit_reasons,
|
||||
"event_types": event_types,
|
||||
"trade_attribution": trade_attribution,
|
||||
"watch_order_attribution": watch_order_attribution,
|
||||
"recent_trades": trades,
|
||||
"recent_events": events,
|
||||
}
|
||||
|
||||
@ -122,9 +122,15 @@ def get_strategy_insights():
|
||||
trade_env_map = {}
|
||||
trade_evidence_map = {}
|
||||
trade_version_map = {}
|
||||
trade_factor_group_map = {}
|
||||
trade_regime_map = {}
|
||||
trade_score_band_map = {}
|
||||
watch_map = {}
|
||||
order_map = {}
|
||||
for item in items:
|
||||
labels = safe_list_json(item.get("signal_labels_json")) or safe_list_json(item.get("signals"))
|
||||
codes = safe_list_json(item.get("signal_codes_json"))
|
||||
ep = safe_dict_json(item.get("entry_plan_json"))
|
||||
for factor in labels:
|
||||
add_bucket(factor_map, str(factor).strip(), item)
|
||||
for code in codes:
|
||||
@ -134,21 +140,38 @@ def get_strategy_insights():
|
||||
elif text.startswith(("dex_", "liquidity_", "exchange_", "whale_", "smart_money", "holder_")):
|
||||
add_bucket(evidence_map, "链上:" + text, item)
|
||||
mc = safe_dict_json(item.get("market_context_json"))
|
||||
factor_breakdown = safe_dict_json(mc.get("factor_score_breakdown")) or safe_dict_json(ep.get("factor_score_breakdown"))
|
||||
score_components = safe_dict_json(mc.get("score_components")) or safe_dict_json(ep.get("score_components"))
|
||||
market_regime = safe_dict_json(mc.get("market_regime")) or safe_dict_json(ep.get("market_regime"))
|
||||
regime_name = market_regime.get("regime") or mc.get("market_regime")
|
||||
for key in ("btc_trend", "market_regime", "altcoin_regime", "sentiment"):
|
||||
if mc.get(key):
|
||||
add_bucket(env_map, f"{key}:{mc.get(key)}", item)
|
||||
if regime_name:
|
||||
add_bucket(env_map, f"regime:{regime_name}", item)
|
||||
for bucket in env_buckets_from_market_context(mc):
|
||||
add_bucket(env_map, bucket, item)
|
||||
if item.get("strategy_version"):
|
||||
add_bucket(version_map, str(item.get("strategy_version")).strip(), item)
|
||||
if (item.get("execution_status") or "") in {"observe", "wait_pullback"} or (item.get("display_bucket") or "") == "watch_pool":
|
||||
add_watch_bucket(watch_map, watch_bucket(item), item)
|
||||
if item.get("paper_order_id"):
|
||||
add_order_bucket(order_map, order_bucket(item), item)
|
||||
if item.get("paper_status") == "closed":
|
||||
for factor in labels:
|
||||
add_trade_bucket(trade_factor_map, str(factor).strip(), item)
|
||||
for group in factor_groups_from_breakdown(factor_breakdown):
|
||||
add_trade_bucket(trade_factor_group_map, group, item)
|
||||
add_trade_bucket(trade_entry_map, trade_entry_bucket(item), item)
|
||||
add_trade_bucket(trade_exit_map, item.get("paper_exit_reason") or "未记录退出原因", item)
|
||||
add_trade_bucket(trade_entry_map, f"方向:{item.get('paper_side') or item.get('side') or 'long'}", item)
|
||||
if item.get("paper_order_id"):
|
||||
add_trade_bucket(trade_entry_map, f"挂单路径:{item.get('paper_order_status') or 'filled'}", item)
|
||||
add_trade_bucket(trade_score_band_map, score_band("机会分", score_components.get("opportunity_score")), item)
|
||||
add_trade_bucket(trade_score_band_map, score_band("买点分", score_components.get("entry_score")), item)
|
||||
add_trade_bucket(trade_score_band_map, score_band("风险分", score_components.get("risk_score")), item)
|
||||
if regime_name:
|
||||
add_trade_bucket(trade_regime_map, f"regime:{regime_name}", item)
|
||||
for bucket in env_buckets_from_market_context(mc):
|
||||
add_trade_bucket(trade_env_map, bucket, item)
|
||||
for code in codes:
|
||||
@ -175,12 +198,20 @@ def get_strategy_insights():
|
||||
"trade_attribution": {
|
||||
"definition": "交易级归因只统计已平仓策略交易,用 realized_pnl_usdt / realized_pnl_pct 衡量因子、入场路径、退出原因和环境的真实账本表现。",
|
||||
"factor": serialize_trade_buckets("factor", trade_factor_map)[:30],
|
||||
"factor_group": serialize_trade_buckets("factor_group", trade_factor_group_map)[:20],
|
||||
"entry_path": serialize_trade_buckets("entry_path", trade_entry_map)[:20],
|
||||
"exit_reason": serialize_trade_buckets("exit_reason", trade_exit_map)[:20],
|
||||
"market_regime": serialize_trade_buckets("market_regime", trade_regime_map)[:20],
|
||||
"score_band": serialize_trade_buckets("score_band", trade_score_band_map)[:20],
|
||||
"market_environment": serialize_trade_buckets("environment", trade_env_map)[:20],
|
||||
"evidence": serialize_trade_buckets("evidence", trade_evidence_map)[:20],
|
||||
"strategy_version": serialize_trade_buckets("strategy_version", trade_version_map, sort_by_version=True)[:20],
|
||||
},
|
||||
"watch_order_attribution": {
|
||||
"definition": "观察池和挂单池只评价机会是否推进,不计入交易收益;用于判断没买/等回踩是否合理。",
|
||||
"watch_pool": serialize_watch_buckets("watch_bucket", watch_map)[:20],
|
||||
"paper_orders": serialize_order_buckets("order_bucket", order_map)[:20],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -221,6 +252,122 @@ def trade_entry_bucket(item):
|
||||
return "入场:未标记"
|
||||
|
||||
|
||||
def factor_groups_from_breakdown(breakdown):
|
||||
groups = breakdown.get("groups") if isinstance(breakdown, dict) else {}
|
||||
if isinstance(groups, dict):
|
||||
return [str(k) for k, v in groups.items() if float((v or {}).get("score") or 0) != 0]
|
||||
items = breakdown.get("items") if isinstance(breakdown, dict) else []
|
||||
result = []
|
||||
for item in items if isinstance(items, list) else []:
|
||||
group = str((item or {}).get("factor_group") or "").strip()
|
||||
if group:
|
||||
result.append(group)
|
||||
return sorted(set(result))
|
||||
|
||||
|
||||
def score_band(label, value):
|
||||
try:
|
||||
n = float(value)
|
||||
except Exception:
|
||||
return f"{label}:未知"
|
||||
if n >= 8:
|
||||
band = "高"
|
||||
elif n >= 3:
|
||||
band = "中"
|
||||
elif n >= 0:
|
||||
band = "低"
|
||||
else:
|
||||
band = "负"
|
||||
return f"{label}:{band}({n:g})"
|
||||
|
||||
|
||||
def watch_bucket(item):
|
||||
status = str(item.get("execution_status") or item.get("display_bucket") or "watch").strip()
|
||||
if status == "wait_pullback":
|
||||
return "观察:等待回踩"
|
||||
if status == "observe":
|
||||
return "观察:普通观察"
|
||||
return f"观察:{status or '未标记'}"
|
||||
|
||||
|
||||
def order_bucket(item):
|
||||
status = str(item.get("paper_order_status") or "unknown").strip()
|
||||
source = str(item.get("paper_source_status") or item.get("execution_status") or "").strip()
|
||||
if status == "filled":
|
||||
return "挂单:已成交"
|
||||
if status == "canceled":
|
||||
return "挂单:已取消"
|
||||
if status == "pending":
|
||||
return "挂单:等待中"
|
||||
return f"挂单:{source or status}"
|
||||
|
||||
|
||||
def add_watch_bucket(bucket_map, key, item):
|
||||
if not key:
|
||||
return
|
||||
b = bucket_map.setdefault(key, {
|
||||
"opportunity_count": 0,
|
||||
"executed_count": 0,
|
||||
"order_count": 0,
|
||||
"invalid_count": 0,
|
||||
})
|
||||
b["opportunity_count"] += 1
|
||||
if item.get("paper_trade_id"):
|
||||
b["executed_count"] += 1
|
||||
if item.get("paper_order_id"):
|
||||
b["order_count"] += 1
|
||||
if (item.get("execution_status") or "") == "invalid" or (item.get("status") or "") in {"expired", "invalid", "archived"}:
|
||||
b["invalid_count"] += 1
|
||||
|
||||
|
||||
def add_order_bucket(bucket_map, key, item):
|
||||
if not key:
|
||||
return
|
||||
b = bucket_map.setdefault(key, {
|
||||
"order_count": 0,
|
||||
"filled_count": 0,
|
||||
"canceled_count": 0,
|
||||
"trade_count": 0,
|
||||
})
|
||||
status = str(item.get("paper_order_status") or "")
|
||||
b["order_count"] += 1
|
||||
if status == "filled":
|
||||
b["filled_count"] += 1
|
||||
if status == "canceled":
|
||||
b["canceled_count"] += 1
|
||||
if item.get("paper_trade_id"):
|
||||
b["trade_count"] += 1
|
||||
|
||||
|
||||
def serialize_watch_buckets(name_key, bucket_map):
|
||||
rows = []
|
||||
for key, bucket in bucket_map.items():
|
||||
total = bucket["opportunity_count"]
|
||||
rows.append({
|
||||
name_key: key,
|
||||
**bucket,
|
||||
"executed_pct": round(bucket["executed_count"] / total * 100, 1) if total else 0,
|
||||
"order_pct": round(bucket["order_count"] / total * 100, 1) if total else 0,
|
||||
"invalid_pct": round(bucket["invalid_count"] / total * 100, 1) if total else 0,
|
||||
})
|
||||
rows.sort(key=lambda x: (-x["opportunity_count"], x[name_key]))
|
||||
return rows
|
||||
|
||||
|
||||
def serialize_order_buckets(name_key, bucket_map):
|
||||
rows = []
|
||||
for key, bucket in bucket_map.items():
|
||||
total = bucket["order_count"]
|
||||
rows.append({
|
||||
name_key: key,
|
||||
**bucket,
|
||||
"fill_pct": round(bucket["filled_count"] / total * 100, 1) if total else 0,
|
||||
"cancel_pct": round(bucket["canceled_count"] / total * 100, 1) if total else 0,
|
||||
})
|
||||
rows.sort(key=lambda x: (-x["order_count"], x[name_key]))
|
||||
return rows
|
||||
|
||||
|
||||
def env_buckets_from_market_context(mc):
|
||||
"""Convert market_context_json numeric fields into attribution buckets."""
|
||||
buckets = []
|
||||
|
||||
@ -45,8 +45,9 @@
|
||||
|
||||
待增强:
|
||||
|
||||
- 风控结果写入返回结果和操作日志,方便复盘。
|
||||
- 引入更丰富的组合风险:相关性、同板块集中度、同方向拥挤度。
|
||||
- 已把风控结果写入开仓事件返回与操作日志,方便复盘。
|
||||
- 已加入同板块集中度、同方向拥挤度门禁。
|
||||
- 后续可继续做更细的相关性矩阵和板块 Beta 暴露。
|
||||
|
||||
### 2. Market Regime Engine
|
||||
|
||||
@ -83,8 +84,8 @@
|
||||
|
||||
待增强:
|
||||
|
||||
- 让分组上限按 market regime 动态调整。
|
||||
- 做因子组级别交易归因,确认哪些组在不同市场下真正贡献收益。
|
||||
- 已做因子组级别交易归因,能在策略归因和复盘中心查看。
|
||||
- 后续让分组上限按 market regime 动态调整。
|
||||
|
||||
### 4. Entry Quality Score
|
||||
|
||||
@ -97,7 +98,8 @@
|
||||
|
||||
待增强:
|
||||
|
||||
- 把三分制进一步接入 `apply_entry_quality_gate`,明确用 `entryScore` 决定 `buy_now` / `wait_pullback` / `watch`。
|
||||
- 已把三分制接入 `apply_entry_quality_gate`:`entry_score` 不足时禁止 `buy_now`,过低时不进入挂单池,转观察。
|
||||
- 后续根据线上样本校准 `min_entry_score_buy_now` / `min_entry_score_wait_pullback`。
|
||||
|
||||
### 5. 完整结构化决策日志
|
||||
|
||||
@ -138,8 +140,8 @@
|
||||
|
||||
## 下一步执行建议
|
||||
|
||||
1. 部署运行一段时间,观察 `market_regime`、`score_components`、`factor_score_breakdown.groups` 是否能解释真实回撤。
|
||||
2. 做 Paper Trade Attribution,把收益按因子组和 regime 归因。
|
||||
3. 把三分制进一步接入买点质量闸门,明确高机会分但低买点分时只能挂单或观察。
|
||||
1. 部署运行一段时间,观察 `market_regime`、`score_components`、`factor_score_breakdown.groups`、观察/挂单推进率是否能解释真实回撤。
|
||||
2. 按线上样本校准 `entry_score` 门槛、group cap 和 high-risk 门槛。
|
||||
3. 做 Regime-based Scoring,让不同市场环境使用不同因子权重和分组上限。
|
||||
4. 如果 JSON 决策日志查询不方便,再新增 `strategy_decision_log` 表和页面。
|
||||
5. 按线上样本继续调整 group cap 和 high-risk 门槛。
|
||||
5. 后续再接入 BTC Dominance、TOTAL3、稳定币净流,增强 Market Regime Engine。
|
||||
|
||||
@ -161,6 +161,8 @@ confirm:
|
||||
low_plan_max_gain_24h_pct: 8
|
||||
low_plan_min_static_count: 3
|
||||
low_plan_min_top_long_pct: 55
|
||||
min_entry_score_buy_now: 3
|
||||
min_entry_score_wait_pullback: 1
|
||||
note: v1.7.5机会生命周期+买点硬闸门;低位静K蓄力生成潜伏计划,高位确认/rr不合格/risk_reward_ok=false不得显示可即刻买入
|
||||
atr_multiplier:
|
||||
entry_offset: 0.5
|
||||
|
||||
@ -56,7 +56,7 @@ function rows(items,label,value,sub){items=items||[];if(!items.length)return '<d
|
||||
function digestItems(items,label,sub){items=items||[];if(!items.length)return '<div class="digest-empty">暂无动作</div>';return items.slice(0,4).map(function(x){return '<div class="digest-item"><b>'+esc(label(x))+'</b><span>'+esc(sub?sub(x):'')+'</span></div>'}).join('')}
|
||||
function renderStrategyDigest(d){var it=d.iteration||{},dig=it.digest||{},latest=dig.latest||{},m=latest.metrics||{},decision=latest.decision||'hold';var badgeCls=decision==='release'?'ok':decision==='gray'?'warn':'warn';$('strategyDigest').innerHTML='<div class="digest-head"><div><div class="digest-title">策略迭代摘要</div><div class="digest-sub">'+esc(latest.title||'暂无复盘')+' · '+time(latest.time)+' · 版本 '+esc(latest.strategy_version||'--')+'</div></div><span class="badge '+badgeCls+'">'+esc(decision)+'</span></div><div class="kpis">'+[['因子生效调整',m.factor_weight_updates||0,'blue'],['候选 / 灰度',(it.summary&&it.summary.candidate_count||0)+' / '+(it.summary&&it.summary.gray_count||0),''],['本轮有效复盘',m.effective_review_count||0,''],['发布状态',latest.reason||'继续观察','']].map(function(x){return '<div class="kpi"><span>'+esc(x[0])+'</span><b class="'+(x[2]||'')+'">'+esc(x[1])+'</b></div>'}).join('')+'</div><div class="digest-grid"><div class="digest-card"><h3>升权了什么</h3>'+digestItems(dig.upgraded,function(x){return x.signal||'--'},function(x){return '权重 '+x.old_weight+' → '+x.new_weight+' · 样本 '+(x.sample_size||0)+' · 命中 '+(x.hit_rate||0)+'%'})+'</div><div class="digest-card"><h3>降权 / 淘汰</h3>'+digestItems(dig.downgraded,function(x){return x.signal||'--'},function(x){return (x.action||'调整')+' · '+(x.old_weight!=null?'权重 '+x.old_weight+' → '+x.new_weight:x.detail||'')})+'</div><div class="digest-card"><h3>灰度观察</h3>'+digestItems(dig.gray,function(x){return x.signal||('候选 #'+x.id)},function(x){return '样本 '+(x.sample_size||0)+' · 置信 '+(x.confidence||0)+'% · '+(x.description||'')})+'</div><div class="digest-card"><h3>最近迭代</h3>'+digestItems(dig.timeline,function(x){return time(x.time)+' · '+(x.decision||'hold')},function(x){return x.reason||x.title||''})+'</div></div>'}
|
||||
function renderOpportunity(o){var s=o.summary||{};$('oppDef').textContent=o.definition||'';$('opportunityPanel').innerHTML=kpis([['机会样本',s.total_opportunities||0,'blue'],['可买/等回踩',(s.buy_now_count||0)+' / '+(s.wait_pullback_count||0),''],['策略执行',s.paper_executed_count||0,'green'],['漏选爆发',s.missed_explosion_count||0,'red'],['有效复盘',s.effective_review_count||0,''],['机会命中',num(s.opportunity_hit_rate,1)+'%','green'],['观察样本',s.observe_count||0,''],['失效样本',s.invalid_count||0,'red']])+'<div class="split"><div><div class="mini-title">状态分布</div>'+rows(o.status_distribution,function(x){return x.name||'--'},function(x){return x.count})+'</div><div><div class="mini-title">复盘结果</div>'+rows(o.outcome_distribution,function(x){return x.name||'--'},function(x){return x.count})+'</div></div>'}
|
||||
function renderPaper(p){var s=p.summary||{},ta=p.trade_attribution||{},tf=ta.factor||[],te=ta.entry_path||[];$('paperDef').textContent=p.definition||'';$('paperPanel').innerHTML=kpis([['当前余额','$'+num(s.current_balance_usdt,2),'blue'],['总收益',usd(s.total_pnl_usdt),Number(s.total_pnl_usdt||0)>=0?'green':'red'],['账户收益率',pct(s.account_total_return_pct),Number(s.account_total_return_pct||0)>=0?'green':'red'],['胜率',num(s.win_rate,1)+'%',''],['开仓/平仓',(s.open_count||0)+' / '+(s.closed_count||0),''],['已实现',usd(s.realized_pnl_usdt),Number(s.realized_pnl_usdt||0)>=0?'green':'red'],['未实现',usd(s.open_unrealized_pnl_usdt),Number(s.open_unrealized_pnl_usdt||0)>=0?'green':'red'],['累计杠杆',num(s.cumulative_leverage,2)+'x','']])+'<div class="split"><div><div class="mini-title">退出原因</div>'+rows(p.exit_reasons,function(x){return x.name||'--'},function(x){return x.count})+'</div><div><div class="mini-title">执行事件</div>'+rows(p.event_types,function(x){return x.name||'--'},function(x){return x.count})+'</div><div><div class="mini-title">真实交易因子</div>'+rows(tf.slice(0,6),function(x){return x.factor||'--'},function(x){return usd(x.realized_pnl_usdt)},function(x){return '平仓 '+(x.closed_trade_count||0)+' · 胜率 '+num(x.win_rate_pct,1)+'% · 均值 '+pct(x.avg_realized_pnl_pct)})+'</div><div><div class="mini-title">入场路径表现</div>'+rows(te.slice(0,6),function(x){return x.entry_path||'--'},function(x){return usd(x.realized_pnl_usdt)},function(x){return '平仓 '+(x.closed_trade_count||0)+' · 胜率 '+num(x.win_rate_pct,1)+'%'})+'</div></div>'}
|
||||
function renderPaper(p){var s=p.summary||{},ta=p.trade_attribution||{},wo=p.watch_order_attribution||{},tf=ta.factor||[],te=ta.entry_path||[],tg=ta.factor_group||[],wr=wo.watch_pool||[],orows=wo.paper_orders||[];$('paperDef').textContent=p.definition||'';$('paperPanel').innerHTML=kpis([['当前余额','$'+num(s.current_balance_usdt,2),'blue'],['总收益',usd(s.total_pnl_usdt),Number(s.total_pnl_usdt||0)>=0?'green':'red'],['账户收益率',pct(s.account_total_return_pct),Number(s.account_total_return_pct||0)>=0?'green':'red'],['胜率',num(s.win_rate,1)+'%',''],['开仓/平仓',(s.open_count||0)+' / '+(s.closed_count||0),''],['已实现',usd(s.realized_pnl_usdt),Number(s.realized_pnl_usdt||0)>=0?'green':'red'],['未实现',usd(s.open_unrealized_pnl_usdt),Number(s.open_unrealized_pnl_usdt||0)>=0?'green':'red'],['累计杠杆',num(s.cumulative_leverage,2)+'x','']])+'<div class="split"><div><div class="mini-title">退出原因</div>'+rows(p.exit_reasons,function(x){return x.name||'--'},function(x){return x.count})+'</div><div><div class="mini-title">执行事件</div>'+rows(p.event_types,function(x){return x.name||'--'},function(x){return x.count})+'</div><div><div class="mini-title">真实交易因子</div>'+rows(tf.slice(0,6),function(x){return x.factor||'--'},function(x){return usd(x.realized_pnl_usdt)},function(x){return '平仓 '+(x.closed_trade_count||0)+' · 胜率 '+num(x.win_rate_pct,1)+'% · 均值 '+pct(x.avg_realized_pnl_pct)})+'</div><div><div class="mini-title">因子组表现</div>'+rows(tg.slice(0,6),function(x){return x.factor_group||'--'},function(x){return usd(x.realized_pnl_usdt)},function(x){return '平仓 '+(x.closed_trade_count||0)+' · 胜率 '+num(x.win_rate_pct,1)+'%'})+'</div><div><div class="mini-title">入场路径表现</div>'+rows(te.slice(0,6),function(x){return x.entry_path||'--'},function(x){return usd(x.realized_pnl_usdt)},function(x){return '平仓 '+(x.closed_trade_count||0)+' · 胜率 '+num(x.win_rate_pct,1)+'%'})+'</div><div><div class="mini-title">观察/挂单推进</div>'+rows(wr.concat(orows).slice(0,6),function(x){return x.watch_bucket||x.order_bucket||'--'},function(x){return (x.executed_pct!=null?num(x.executed_pct,1):num(x.fill_pct,1))+'%'},function(x){return '样本 '+(x.opportunity_count||x.order_count||0)+' · 执行/成交 '+(x.executed_count||x.filled_count||0)})+'</div></div>'}
|
||||
function renderEvidence(e){var s=e.summary||{};$('evidenceDef').textContent=e.definition||'';$('evidencePanel').innerHTML=kpis([['新闻事件',s.news_count||0,'blue'],['有效舆情',s.actionable_news_count||0,''],['链上信号',s.onchain_signal_count||0,'blue'],['高置信链上',s.high_confidence_onchain_count||0,'green'],['原始链上',s.raw_onchain_count||0,''],['已映射原始',s.mapped_raw_onchain_count||0,'green'],['LLM 调用',s.llm_runs||0,''],['LLM 成功',s.llm_success_count||0,'green']])+'<div class="split"><div><div class="mini-title">链上信号</div>'+rows(e.onchain_signals,function(x){return x.name||'--'},function(x){return x.count})+'</div><div><div class="mini-title">舆情决策</div>'+rows(e.news_decisions,function(x){return x.name||'未处理'},function(x){return x.count})+'</div></div>'}
|
||||
function renderIteration(i){var s=i.summary||{};$('iterationDef').textContent=i.definition||'';$('iterationPanel').innerHTML=kpis([['迭代记录',s.iteration_count||0,'blue'],['候选规则',s.candidate_count||0,''],['灰度规则',s.gray_count||0,'green'],['生效规则',s.active_count||0,'green']])+'<div class="row" style="margin-bottom:10px"><div class="row-main"><div class="row-title">最新发布结论:'+esc(s.latest_release_decision||'hold')+'</div><div class="row-sub">'+esc(s.latest_release_reason||'暂无发布说明')+'</div></div><div class="value">闸门</div></div><div class="split"><div><div class="mini-title">发布决策</div>'+rows(i.release_decisions,function(x){return x.name||'--'},function(x){return x.count})+'</div><div><div class="mini-title">候选状态</div>'+rows(i.candidate_status,function(x){return x.name||'--'},function(x){return x.count})+'</div></div>'}
|
||||
function renderRecent(d){var opp=(d.opportunity&&d.opportunity.missed_explosions)||[], trades=(d.paper_trading&&d.paper_trading.recent_trades)||[], news=(d.evidence&&d.evidence.recent_news)||[], chain=(d.evidence&&d.evidence.recent_onchain)||[];$('recentPanel').innerHTML='<div class="split"><div><div class="mini-title">最近策略交易</div>'+rows(trades.slice(0,8),function(x){return (x.symbol||'--')+' · '+(x.status||'--')},function(x){return x.status==='closed'?pct(x.realized_pnl_pct):pct(x.pnl_pct)},function(x){return time(x.opened_at)+' · '+(x.exit_reason||x.source_status||'')})+'</div><div><div class="mini-title">漏选爆发</div>'+rows(opp.slice(0,8),function(x){return x.symbol||'--'},function(x){return pct(x.gain_pct)},function(x){return time(x.detect_time)+' · '+(x.reason_missed||'')})+'</div><div><div class="mini-title">舆情事件</div>'+rows(news.slice(0,8),function(x){return (x.symbol||'--')+' · '+(x.title||'--')},function(x){return x.importance||'--'},function(x){return time(x.detected_at)+' · '+(x.source||'')+' · '+(x.decision||'未处理')})+'</div><div><div class="mini-title">链上信号</div>'+rows(chain.slice(0,8),function(x){return (x.symbol||'--')+' · '+(x.signal_label||x.signal_code||'--')},function(x){return x.severity||x.confidence||'--'},function(x){return time(x.detected_at)+' · '+(x.source||'')+' · '+(x.direction||'')})+'</div></div>'}
|
||||
|
||||
@ -37,7 +37,8 @@ function esc(v){return String(v==null?'':v).replace(/[&<>"']/g,function(c){retur
|
||||
function pct(x){return Number(x||0).toFixed(1)+'%'}function usd(x){x=Number(x||0);return (x>=0?'+':'-')+'$'+Math.abs(x).toFixed(2)}
|
||||
function table(rows,key){if(!rows||!rows.length)return'<div class="empty">暂无数据</div>';return '<div class="table-wrap"><table class="table"><thead><tr><th>归因项</th><th>机会</th><th>可执行</th><th>现买</th><th>策略执行</th><th>执行转化</th><th>策略胜率</th><th>策略已实现</th></tr></thead><tbody>'+rows.map(function(r){return '<tr><td><span class="tag">'+esc(r[key]||'--')+'</span></td><td class="num">'+esc(r.opportunity_count||0)+'</td><td>'+esc(r.actionable_count||0)+'</td><td>'+esc(r.buy_now_count||0)+'</td><td>'+esc(r.paper_trade_count||0)+'</td><td class="num">'+pct(r.actionable_conversion_pct)+'</td><td class="num">'+pct(r.paper_win_rate_pct)+'</td><td class="'+(Number(r.paper_realized_pnl_usdt||0)>=0?'green':'red')+'">'+usd(r.paper_realized_pnl_usdt)+'</td></tr>'}).join('')+'</tbody></table></div>'}
|
||||
function tradeTable(title,rows,key){if(!rows||!rows.length)return '<div><div class="panel-note" style="margin:8px 0">'+esc(title)+'</div><div class="empty">暂无已平仓交易样本</div></div>';return '<div><div class="panel-note" style="margin:8px 0">'+esc(title)+'</div><div class="table-wrap"><table class="table"><thead><tr><th>归因项</th><th>平仓交易</th><th>胜率</th><th>已实现收益</th><th>平均收益率</th><th>最好</th><th>最差</th></tr></thead><tbody>'+rows.map(function(r){return '<tr><td><span class="tag">'+esc(r[key]||'--')+'</span></td><td class="num">'+esc(r.closed_trade_count||0)+'</td><td class="num">'+pct(r.win_rate_pct)+'</td><td class="'+(Number(r.realized_pnl_usdt||0)>=0?'green':'red')+'">'+usd(r.realized_pnl_usdt)+'</td><td class="'+(Number(r.avg_realized_pnl_pct||0)>=0?'green':'red')+'">'+pct(r.avg_realized_pnl_pct)+'</td><td class="green">'+pct(r.best_pnl_pct)+'</td><td class="red">'+pct(r.worst_pnl_pct)+'</td></tr>'}).join('')+'</tbody></table></div></div>'}
|
||||
function renderTradeAttribution(t){t=t||{};tradeDef.textContent=t.definition||'交易级归因只统计已平仓策略交易。';tradePerf.innerHTML=tradeTable('因子真实交易表现',t.factor,'factor')+tradeTable('入场路径与方向',t.entry_path,'entry_path')+tradeTable('退出原因',t.exit_reason,'exit_reason')+tradeTable('市场环境',t.market_environment,'environment')}
|
||||
async function load(){try{var d=await (await fetch('/api/strategy/insights?_ts='+Date.now(),{cache:'no-store'})).json();var o=d.overview||{};definition.textContent=o.definition||'策略归因只看机会转化和策略交易转化。';metrics.innerHTML=[['机会样本',o.total_opportunities||0,'blue'],['可执行机会',o.actionable_count||0,''],['现在可买',o.buy_now_count||0,'green'],['策略执行',o.paper_trade_count||0,'green'],['策略胜率',pct(o.paper_win_rate_pct),''],['已实现收益',usd(o.paper_realized_pnl_usdt),Number(o.paper_realized_pnl_usdt||0)>=0?'green':'red']].map(function(x){return '<div class="kpi"><span>'+esc(x[0])+'</span><b class="'+(x[2]||'')+'">'+esc(x[1])+'</b></div>'}).join('');factorPerf.innerHTML=table(d.factor_attribution,'factor');envPerf.innerHTML=table(d.market_environment,'environment');evidencePerf.innerHTML=table(d.evidence_attribution,'evidence');versionPerf.innerHTML=table(d.version_performance,'strategy_version');renderTradeAttribution(d.trade_attribution)}catch(e){metrics.innerHTML='<div class="empty">加载失败</div>'}}load();
|
||||
function watchTable(title,rows,key){if(!rows||!rows.length)return '<div><div class="panel-note" style="margin:8px 0">'+esc(title)+'</div><div class="empty">暂无观察/挂单样本</div></div>';return '<div><div class="panel-note" style="margin:8px 0">'+esc(title)+'</div><div class="table-wrap"><table class="table"><thead><tr><th>归因项</th><th>样本</th><th>执行/成交</th><th>挂单/取消</th><th>转化率</th></tr></thead><tbody>'+rows.map(function(r){var isOrder=key==='order_bucket';return '<tr><td><span class="tag">'+esc(r[key]||'--')+'</span></td><td class="num">'+esc(r.opportunity_count||r.order_count||0)+'</td><td>'+esc(isOrder?(r.filled_count||0):(r.executed_count||0))+'</td><td>'+esc(isOrder?(r.canceled_count||0):(r.order_count||0))+'</td><td class="num">'+pct(isOrder?r.fill_pct:r.executed_pct)+'</td></tr>'}).join('')+'</tbody></table></div></div>'}
|
||||
function renderTradeAttribution(t,w){t=t||{};w=w||{};tradeDef.textContent=t.definition||'交易级归因只统计已平仓策略交易。';tradePerf.innerHTML=tradeTable('因子真实交易表现',t.factor,'factor')+tradeTable('因子组表现',t.factor_group,'factor_group')+tradeTable('市场状态表现',t.market_regime,'market_regime')+tradeTable('评分分层表现',t.score_band,'score_band')+tradeTable('入场路径与方向',t.entry_path,'entry_path')+tradeTable('退出原因',t.exit_reason,'exit_reason')+tradeTable('市场环境',t.market_environment,'environment')+watchTable('观察池推进表现',w.watch_pool,'watch_bucket')+watchTable('挂单池成交/取消表现',w.paper_orders,'order_bucket')}
|
||||
async function load(){try{var d=await (await fetch('/api/strategy/insights?_ts='+Date.now(),{cache:'no-store'})).json();var o=d.overview||{};definition.textContent=o.definition||'策略归因只看机会转化和策略交易转化。';metrics.innerHTML=[['机会样本',o.total_opportunities||0,'blue'],['可执行机会',o.actionable_count||0,''],['现在可买',o.buy_now_count||0,'green'],['策略执行',o.paper_trade_count||0,'green'],['策略胜率',pct(o.paper_win_rate_pct),''],['已实现收益',usd(o.paper_realized_pnl_usdt),Number(o.paper_realized_pnl_usdt||0)>=0?'green':'red']].map(function(x){return '<div class="kpi"><span>'+esc(x[0])+'</span><b class="'+(x[2]||'')+'">'+esc(x[1])+'</b></div>'}).join('');factorPerf.innerHTML=table(d.factor_attribution,'factor');envPerf.innerHTML=table(d.market_environment,'environment');evidencePerf.innerHTML=table(d.evidence_attribution,'evidence');versionPerf.innerHTML=table(d.version_performance,'strategy_version');renderTradeAttribution(d.trade_attribution,d.watch_order_attribution)}catch(e){metrics.innerHTML='<div class="empty">加载失败</div>'}}load();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
from app.core.market_regime import classify_market_regime
|
||||
from app.core.global_risk import evaluate_global_risk
|
||||
|
||||
|
||||
def _overview(**overrides):
|
||||
@ -45,3 +46,35 @@ def test_market_regime_detects_altcoin_rotation():
|
||||
|
||||
assert result["regime"] == "altcoin_rotation"
|
||||
assert result["risk_level"] == "medium"
|
||||
|
||||
|
||||
def test_global_risk_blocks_same_sector_concentration(pg_conn, monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
"app.core.global_risk.get_crypto_market_overview",
|
||||
lambda allow_live_fallback=False: _overview(),
|
||||
)
|
||||
pg_conn.execute(
|
||||
"""
|
||||
INSERT INTO paper_trades (
|
||||
recommendation_id, symbol, side, status, opened_at, entry_price, qty,
|
||||
notional_usdt, current_price, pnl_pct, created_at, updated_at
|
||||
) VALUES
|
||||
(1, 'DOGE/USDT', 'long', 'open', '2026-05-23T10:00:00', 1, 100, 1000, 1, 0, '2026-05-23T10:00:00', '2026-05-23T10:00:00'),
|
||||
(2, 'SHIB/USDT', 'long', 'open', '2026-05-23T10:00:00', 1, 100, 1000, 1, 0, '2026-05-23T10:00:00', '2026-05-23T10:00:00')
|
||||
"""
|
||||
)
|
||||
pg_conn.commit()
|
||||
|
||||
result = evaluate_global_risk(
|
||||
conn=pg_conn,
|
||||
config={
|
||||
"account_equity_usdt": 20000,
|
||||
"global_risk_max_same_sector_positions": 2,
|
||||
"global_risk_max_same_direction_positions": 10,
|
||||
},
|
||||
rec={"symbol": "PEPE/USDT", "sector": "MEME"},
|
||||
additional_notional=1000,
|
||||
)
|
||||
|
||||
assert result["allow_new_entries"] is False
|
||||
assert result["decision"] == "block_same_sector_concentration"
|
||||
|
||||
@ -52,6 +52,31 @@ def test_buy_now_with_bad_rr_sets_real_pullback_price():
|
||||
assert any('现价不买' in r for r in reasons)
|
||||
|
||||
|
||||
def test_low_entry_score_blocks_buy_now_and_weak_pullback():
|
||||
action, plan, reasons = apply_entry_quality_gate(
|
||||
action_status='可即刻买入',
|
||||
entry_plan={
|
||||
'entry_action': '可即刻买入',
|
||||
'entry_price': 1.0,
|
||||
'current_price': 1.0,
|
||||
'stop_loss': 0.95,
|
||||
'tp1': 1.12,
|
||||
'risk_reward_ok': True,
|
||||
'rr1': 2.4,
|
||||
'entry_trigger_confirmed': True,
|
||||
'score_components': {'opportunity_score': 12, 'entry_score': 0, 'risk_score': 1},
|
||||
},
|
||||
signals=['当前15min即刻入场信号'],
|
||||
current_price=1.0,
|
||||
market_context={'change_24h': 2.0},
|
||||
cfg={'min_entry_score_buy_now': 3, 'min_entry_score_wait_pullback': 1},
|
||||
)
|
||||
|
||||
assert action == '观察'
|
||||
assert any('买点分' in r for r in reasons)
|
||||
assert plan['entry_quality_gate']['entry_score'] == 0
|
||||
|
||||
|
||||
def test_buy_now_requires_current_trigger_and_no_bearish_flow_risk():
|
||||
action, plan, reasons = apply_entry_quality_gate(
|
||||
action_status='可即刻买入',
|
||||
|
||||
@ -83,6 +83,7 @@ def test_review_center_separates_opportunity_and_paper_pnl(pg_conn):
|
||||
assert data["paper_trading"]["summary"]["closed_count"] == 1
|
||||
assert data["paper_trading"]["summary"]["realized_pnl_usdt"] == 490
|
||||
assert data["paper_trading"]["trade_attribution"]["entry_path"][0]["closed_trade_count"] == 1
|
||||
assert "watch_order_attribution" in data["paper_trading"]
|
||||
|
||||
|
||||
def test_review_center_iteration_digest_summarizes_actions(pg_conn):
|
||||
|
||||
Loading…
Reference in New Issue
Block a user