1
This commit is contained in:
parent
ed7b420c57
commit
cca36615c1
@ -107,6 +107,7 @@ AlphaX 是一个以 `Python + FastAPI + PostgreSQL + Docker + 静态 HTML` 组
|
|||||||
- `signal_performance` 是复盘后动态权重来源;`review_engine.py` 更新信号绩效后,`config_loader.get_signal_weights()` 会让下一轮筛选/确认读取生效权重。
|
- `signal_performance` 是复盘后动态权重来源;`review_engine.py` 更新信号绩效后,`config_loader.get_signal_weights()` 会让下一轮筛选/确认读取生效权重。
|
||||||
- 当前确认层已把核心技术因子、资金面因子、板块因子、舆情因子和买点风险因子接入 `FactorScorer`,并在 `market_context.factor_score_breakdown` / `entry_plan.factor_score_breakdown` 中保留因子明细。
|
- 当前确认层已把核心技术因子、资金面因子、板块因子、舆情因子和买点风险因子接入 `FactorScorer`,并在 `market_context.factor_score_breakdown` / `entry_plan.factor_score_breakdown` 中保留因子明细。
|
||||||
- `FactorScorer` 已加入因子组去相关,同一类 `momentum` / `structure` / `entry_quality` / `onchain_flow` / `narrative` 信号会受 group cap 限制,避免同一根行情被重复加分。
|
- `FactorScorer` 已加入因子组去相关,同一类 `momentum` / `structure` / `entry_quality` / `onchain_flow` / `narrative` 信号会受 group cap 限制,避免同一根行情被重复加分。
|
||||||
|
- 小样本复盘不能直接杀死核心因子。`signal_performance` 的动态权重至少要满足 `review.min_samples_for_weight` 与 `review.signal_deprecation.min_samples` 后才覆盖确认层基线;未达样本门槛时只用于观察,不应用 0 权重把 15min 启动、日线突破回踩等因子压没。
|
||||||
- 扣分因子应传负数,例如 `FactorScorer.delta("false_breakout", -5, ...)`,不要再外部 `score -= delta`,否则 `factor_score_breakdown` 会把风险误记成正向贡献。
|
- 扣分因子应传负数,例如 `FactorScorer.delta("false_breakout", -5, ...)`,不要再外部 `score -= delta`,否则 `factor_score_breakdown` 会把风险误记成正向贡献。
|
||||||
- 确认层会输出 `score_components`:`opportunity_score` 表示机会质量,`entry_score` 表示买点质量,`risk_score` 表示扣分风险;后续策略不要再只看单一 `rec_score`。
|
- 确认层会输出 `score_components`:`opportunity_score` 表示机会质量,`entry_score` 表示买点质量,`risk_score` 表示扣分风险;后续策略不要再只看单一 `rec_score`。
|
||||||
- `market_context.decision_log` / `entry_plan.decision_log` 是结构化决策解释;paper trading 开仓事件也会记录当时 `market_regime`、`global_risk` 和 `score_components`。
|
- `market_context.decision_log` / `entry_plan.decision_log` 是结构化决策解释;paper trading 开仓事件也会记录当时 `market_regime`、`global_risk` 和 `score_components`。
|
||||||
@ -163,6 +164,7 @@ AlphaX 是一个以 `Python + FastAPI + PostgreSQL + Docker + 静态 HTML` 组
|
|||||||
- 市场环境识别中心,第一版基于市场快照、BTC/ETH 涨跌、山寨涨跌广度、强势/大跌数量和 funding 热度识别 `risk_off`、`btc_main_uptrend`、`altcoin_rotation`、`sideways_chop`、`meme_frenzy`、`unknown`。
|
- 市场环境识别中心,第一版基于市场快照、BTC/ETH 涨跌、山寨涨跌广度、强势/大跌数量和 funding 热度识别 `risk_off`、`btc_main_uptrend`、`altcoin_rotation`、`sideways_chop`、`meme_frenzy`、`unknown`。
|
||||||
- `app/core/global_risk.py`
|
- `app/core/global_risk.py`
|
||||||
- paper trading 全局风控门禁。单币机会进入开仓或挂单成交前,需要先检查市场环境和账户风险;critical 禁止新开仓,high 只允许高质量机会。
|
- paper trading 全局风控门禁。单币机会进入开仓或挂单成交前,需要先检查市场环境和账户风险;critical 禁止新开仓,high 只允许高质量机会。
|
||||||
|
- 确认层也会应用同一市场风控语义:`risk_level=critical` 且 `position_multiplier=0` 时,强势发现仍可记录为观察,但不能输出 `buy_now` 或新挂单动作;已有活跃可交易推荐会被降级为观察并写入 `market_risk_gate`。
|
||||||
|
|
||||||
## 5. 数据与状态中心
|
## 5. 数据与状态中心
|
||||||
|
|
||||||
|
|||||||
@ -267,7 +267,10 @@ def get_signal_weights():
|
|||||||
- 旧脚本仍可能用历史 key(如 "1H放量(量价背离)")直接查 weights[...]
|
- 旧脚本仍可能用历史 key(如 "1H放量(量价背离)")直接查 weights[...]
|
||||||
因此返回值同时暴露 canonical key + alias key,避免旧调用方 KeyError。
|
因此返回值同时暴露 canonical key + alias key,避免旧调用方 KeyError。
|
||||||
"""
|
"""
|
||||||
rules = load_rules()
|
# Signal weights need a stable baseline. Runtime strategy_override may
|
||||||
|
# contain small-sample governance writes; those are only trusted through
|
||||||
|
# signal_performance after the sample-size gate below.
|
||||||
|
rules = _load_yaml_baseline()
|
||||||
yaml_weights = copy.deepcopy(rules.get("signal_weights", {}))
|
yaml_weights = copy.deepcopy(rules.get("signal_weights", {}))
|
||||||
|
|
||||||
canonical = {}
|
canonical = {}
|
||||||
@ -277,9 +280,15 @@ def get_signal_weights():
|
|||||||
try:
|
try:
|
||||||
from app.db.altcoin_db import get_signal_weights as db_get_weights
|
from app.db.altcoin_db import get_signal_weights as db_get_weights
|
||||||
db_weights = db_get_weights()
|
db_weights = db_get_weights()
|
||||||
|
review_params = get_review_params()
|
||||||
|
deprecation_params = review_params.get("signal_deprecation") or {}
|
||||||
|
min_samples = max(
|
||||||
|
int(review_params.get("min_samples_for_weight", 12) or 12),
|
||||||
|
int(deprecation_params.get("min_samples", 12) or 12),
|
||||||
|
)
|
||||||
for sig, data in db_weights.items():
|
for sig, data in db_weights.items():
|
||||||
norm_sig = normalize_signal_name(sig)
|
norm_sig = normalize_signal_name(sig)
|
||||||
if data.get("total_count", 0) >= 3:
|
if data.get("total_count", 0) >= min_samples:
|
||||||
canonical[norm_sig] = data["weight"]
|
canonical[norm_sig] = data["weight"]
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@ -70,6 +70,11 @@ def expire_old_recommendations(*args, **kwargs):
|
|||||||
return _recommendation_commands.expire_old_recommendations(*args, **kwargs)
|
return _recommendation_commands.expire_old_recommendations(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade_active_entries_for_market_risk(*args, **kwargs):
|
||||||
|
_sync_command_compat_hooks()
|
||||||
|
return _recommendation_commands.downgrade_active_entries_for_market_risk(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def apply_recommendation_state_transition(*args, **kwargs):
|
def apply_recommendation_state_transition(*args, **kwargs):
|
||||||
_sync_command_compat_hooks()
|
_sync_command_compat_hooks()
|
||||||
return _recommendation_commands.apply_recommendation_state_transition(*args, **kwargs)
|
return _recommendation_commands.apply_recommendation_state_transition(*args, **kwargs)
|
||||||
|
|||||||
@ -1390,6 +1390,138 @@ def sync_recommendation(rec: dict, current_price: float, event_time: str = "") -
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def sync_pending_paper_orders(limit: int = 100, event_time: str = "", config: dict | None = None) -> dict:
|
||||||
|
"""Reconcile pending limit orders against the latest shared price cache.
|
||||||
|
|
||||||
|
The strategy runner can miss an existing order if the recommendation is
|
||||||
|
later derived back into observe status. Pending orders are executable
|
||||||
|
state, so they need their own reconciliation pass based on the same
|
||||||
|
latest_price_cache used by the Web page.
|
||||||
|
"""
|
||||||
|
if not paper_trading_enabled():
|
||||||
|
return {"enabled": False, "processed_count": 0, "results": []}
|
||||||
|
limit = max(1, min(_safe_int(limit, 100), 500))
|
||||||
|
event_time = event_time or _now()
|
||||||
|
cfg = _paper_cfg(config)
|
||||||
|
conn = get_conn()
|
||||||
|
results = []
|
||||||
|
try:
|
||||||
|
rows = conn.execute(
|
||||||
|
"""
|
||||||
|
SELECT
|
||||||
|
po.*,
|
||||||
|
r.status AS rec_status,
|
||||||
|
r.rec_state,
|
||||||
|
r.rec_score,
|
||||||
|
r.action_status,
|
||||||
|
r.execution_status,
|
||||||
|
r.lifecycle_state,
|
||||||
|
r.display_bucket,
|
||||||
|
r.entry_price,
|
||||||
|
r.current_price AS rec_current_price,
|
||||||
|
r.stop_loss AS rec_stop_loss,
|
||||||
|
r.tp1 AS rec_tp1,
|
||||||
|
r.tp2 AS rec_tp2,
|
||||||
|
r.strategy_version AS rec_strategy_version,
|
||||||
|
r.entry_plan_json,
|
||||||
|
r.market_context_json,
|
||||||
|
r.derivatives_context_json,
|
||||||
|
r.sector_context_json,
|
||||||
|
lpc.price AS latest_price,
|
||||||
|
lpc.updated_at AS latest_price_updated_at
|
||||||
|
FROM paper_orders po
|
||||||
|
LEFT JOIN recommendation r ON r.id = po.recommendation_id
|
||||||
|
LEFT JOIN latest_price_cache lpc ON lpc.symbol = po.symbol
|
||||||
|
WHERE po.status='pending'
|
||||||
|
ORDER BY po.created_at ASC, po.id ASC
|
||||||
|
LIMIT %s
|
||||||
|
""",
|
||||||
|
(limit,),
|
||||||
|
).fetchall()
|
||||||
|
for row in rows:
|
||||||
|
item = dict(row)
|
||||||
|
current_price = _safe_float(item.get("latest_price"))
|
||||||
|
order = {k: item.get(k) for k in [
|
||||||
|
"id",
|
||||||
|
"recommendation_id",
|
||||||
|
"symbol",
|
||||||
|
"side",
|
||||||
|
"order_type",
|
||||||
|
"status",
|
||||||
|
"source_status",
|
||||||
|
"source_action",
|
||||||
|
"target_price",
|
||||||
|
"current_price_at_create",
|
||||||
|
"fill_price",
|
||||||
|
"notional_usdt",
|
||||||
|
"stop_loss",
|
||||||
|
"tp1",
|
||||||
|
"tp2",
|
||||||
|
"strategy_version",
|
||||||
|
"entry_plan_snapshot_json",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"expires_at",
|
||||||
|
"filled_at",
|
||||||
|
"canceled_at",
|
||||||
|
"cancel_reason",
|
||||||
|
]}
|
||||||
|
rec = {
|
||||||
|
"id": item.get("recommendation_id"),
|
||||||
|
"symbol": item.get("symbol"),
|
||||||
|
"status": item.get("rec_status") or "active",
|
||||||
|
"rec_state": item.get("rec_state"),
|
||||||
|
"rec_score": item.get("rec_score"),
|
||||||
|
"action_status": item.get("action_status") or item.get("source_action"),
|
||||||
|
"execution_status": item.get("execution_status") or item.get("source_status"),
|
||||||
|
"lifecycle_state": item.get("lifecycle_state"),
|
||||||
|
"display_bucket": item.get("display_bucket"),
|
||||||
|
"entry_price": item.get("entry_price") or item.get("target_price"),
|
||||||
|
"current_price": item.get("rec_current_price") or current_price,
|
||||||
|
"stop_loss": item.get("rec_stop_loss") or item.get("stop_loss"),
|
||||||
|
"tp1": item.get("rec_tp1") or item.get("tp1"),
|
||||||
|
"tp2": item.get("rec_tp2") or item.get("tp2"),
|
||||||
|
"strategy_version": item.get("rec_strategy_version") or item.get("strategy_version"),
|
||||||
|
"entry_plan_json": item.get("entry_plan_json") or item.get("entry_plan_snapshot_json"),
|
||||||
|
"market_context_json": item.get("market_context_json"),
|
||||||
|
"derivatives_context_json": item.get("derivatives_context_json"),
|
||||||
|
"sector_context_json": item.get("sector_context_json"),
|
||||||
|
}
|
||||||
|
if current_price <= 0:
|
||||||
|
result = {
|
||||||
|
"skipped": True,
|
||||||
|
"reason": "missing_latest_price",
|
||||||
|
"paper_order_id": order.get("id"),
|
||||||
|
"symbol": order.get("symbol"),
|
||||||
|
"target_price": order.get("target_price"),
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
result = _sync_wait_pullback_order(conn, rec, current_price, event_time, cfg)
|
||||||
|
result.update({
|
||||||
|
"symbol": order.get("symbol"),
|
||||||
|
"latest_price": current_price,
|
||||||
|
"latest_price_updated_at": item.get("latest_price_updated_at") or "",
|
||||||
|
})
|
||||||
|
results.append(result)
|
||||||
|
conn.commit()
|
||||||
|
except Exception:
|
||||||
|
conn.rollback()
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
conn.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return {
|
||||||
|
"enabled": True,
|
||||||
|
"processed_count": len(results),
|
||||||
|
"filled_count": sum(1 for r in results if r.get("paper_order", {}).get("filled")),
|
||||||
|
"canceled_count": sum(1 for r in results if str(r.get("reason") or "").startswith("paper_order_") and "canceled" in str(r.get("reason") or "")),
|
||||||
|
"results": results,
|
||||||
|
"run_time": event_time,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_paper_trading_summary(days: int = 30) -> dict:
|
def get_paper_trading_summary(days: int = 30) -> dict:
|
||||||
days = max(1, min(_safe_int(days, 30), 365))
|
days = max(1, min(_safe_int(days, 30), 365))
|
||||||
cutoff = (datetime.now() - timedelta(days=days)).isoformat()
|
cutoff = (datetime.now() - timedelta(days=days)).isoformat()
|
||||||
|
|||||||
@ -244,6 +244,66 @@ def expire_old_recommendations(hours=48):
|
|||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade_active_entries_for_market_risk(reason: str, event_time: str | None = None) -> dict:
|
||||||
|
"""Downgrade active executable recommendations when global market risk blocks entries."""
|
||||||
|
event_time = event_time or datetime.now().isoformat()
|
||||||
|
reason = str(reason or "全市场风险过高,暂停新开仓与新挂单").strip()
|
||||||
|
conn = get_conn()
|
||||||
|
rows = conn.execute(
|
||||||
|
"""
|
||||||
|
SELECT id, entry_plan_json, action_status
|
||||||
|
FROM recommendation
|
||||||
|
WHERE status='active'
|
||||||
|
AND (
|
||||||
|
COALESCE(action_status,'') IN ('可即刻买入','等回踩')
|
||||||
|
OR COALESCE(execution_status,'') IN ('buy_now','wait_pullback')
|
||||||
|
)
|
||||||
|
ORDER BY id DESC
|
||||||
|
"""
|
||||||
|
).fetchall()
|
||||||
|
updated = 0
|
||||||
|
execution_status, display_bucket, lifecycle_state, entry_triggered, state_reason = derive_minimal_state_fields(
|
||||||
|
"active", "观察", {}
|
||||||
|
)
|
||||||
|
for row in rows:
|
||||||
|
entry_plan = normalize_json_object(row["entry_plan_json"])
|
||||||
|
previous_action = str(row["action_status"] or "").strip()
|
||||||
|
entry_plan["market_risk_gate"] = {
|
||||||
|
"blocked_action": previous_action,
|
||||||
|
"final_action": "观察",
|
||||||
|
"risk_level": "critical",
|
||||||
|
"reasons": [reason],
|
||||||
|
"updated_at": event_time,
|
||||||
|
}
|
||||||
|
entry_plan["entry_action"] = "观察"
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
UPDATE recommendation
|
||||||
|
SET action_status='观察',
|
||||||
|
execution_status=%s,
|
||||||
|
display_bucket=%s,
|
||||||
|
lifecycle_state=%s,
|
||||||
|
entry_triggered=%s,
|
||||||
|
state_reason=%s,
|
||||||
|
entry_plan_json=%s
|
||||||
|
WHERE id=%s
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
execution_status,
|
||||||
|
display_bucket,
|
||||||
|
lifecycle_state,
|
||||||
|
entry_triggered,
|
||||||
|
state_reason,
|
||||||
|
json.dumps(entry_plan, ensure_ascii=False),
|
||||||
|
row["id"],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
updated += 1
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return {"updated_count": updated, "reason": reason, "updated_at": event_time}
|
||||||
|
|
||||||
|
|
||||||
def apply_recommendation_state_transition(rec_id, requested_action, current_price, event_time=None, signals=None):
|
def apply_recommendation_state_transition(rec_id, requested_action, current_price, event_time=None, signals=None):
|
||||||
"""The single DB entry for turning price events into recommendation action state."""
|
"""The single DB entry for turning price events into recommendation action state."""
|
||||||
event_time = event_time or datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
|
event_time = event_time or datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
|
||||||
|
|||||||
@ -29,7 +29,7 @@ from app.core.sector_map import get_burst_threshold, is_meme_coin, get_sector_fo
|
|||||||
from app.db.altcoin_db import (
|
from app.db.altcoin_db import (
|
||||||
init_db, expire_old_states, expire_old_recommendations,
|
init_db, expire_old_states, expire_old_recommendations,
|
||||||
get_candidates_for_confirm, update_state, get_conn, create_recommendation, log_screening,
|
get_candidates_for_confirm, update_state, get_conn, create_recommendation, log_screening,
|
||||||
log_cron_run, update_latest_price_cache,
|
log_cron_run, update_latest_price_cache, downgrade_active_entries_for_market_risk,
|
||||||
)
|
)
|
||||||
from app.config.config_loader import (
|
from app.config.config_loader import (
|
||||||
get_strategy_direction,
|
get_strategy_direction,
|
||||||
@ -524,6 +524,32 @@ def _decision_log(module: str, decision: str, *, score: float = 0.0, reasons=Non
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_market_risk_entry_gate(entry_plan: dict, signals: list, market_regime: dict) -> tuple[dict, str]:
|
||||||
|
"""Keep high-risk market discoveries visible, but block executable entries."""
|
||||||
|
plan = dict(entry_plan or {})
|
||||||
|
regime = market_regime if isinstance(market_regime, dict) else {}
|
||||||
|
risk_level = str(regime.get("risk_level") or "").strip().lower()
|
||||||
|
position_multiplier = float(regime.get("position_multiplier") or 0)
|
||||||
|
current_action = str(plan.get("entry_action") or "").strip()
|
||||||
|
if risk_level != "critical" or position_multiplier > 0:
|
||||||
|
return plan, ""
|
||||||
|
if current_action not in {"可即刻买入", "即刻买入", "等回踩"}:
|
||||||
|
return plan, ""
|
||||||
|
|
||||||
|
reason = "全市场处于 critical 风险,暂停新开仓与新挂单,保留为观察机会"
|
||||||
|
plan["market_risk_gate"] = {
|
||||||
|
"blocked_action": current_action,
|
||||||
|
"final_action": "观察",
|
||||||
|
"risk_level": risk_level,
|
||||||
|
"position_multiplier": position_multiplier,
|
||||||
|
"reasons": [reason],
|
||||||
|
}
|
||||||
|
plan["entry_action"] = "观察"
|
||||||
|
if not any("市场风控闸门" in str(sig) for sig in signals):
|
||||||
|
signals.append(f"⚠️ 市场风控闸门: {reason}")
|
||||||
|
return plan, reason
|
||||||
|
|
||||||
|
|
||||||
# ==================== 确认逻辑 ====================
|
# ==================== 确认逻辑 ====================
|
||||||
|
|
||||||
def detect_volume_price_fly_1h(df_1h):
|
def detect_volume_price_fly_1h(df_1h):
|
||||||
@ -1437,6 +1463,15 @@ def confirm_burst(symbol, cand):
|
|||||||
sector_context = compute_sector_context(symbol, cand_detail)
|
sector_context = compute_sector_context(symbol, cand_detail)
|
||||||
regime_context = _current_market_regime_context()
|
regime_context = _current_market_regime_context()
|
||||||
market_regime = regime_context.get("market_regime") or {}
|
market_regime = regime_context.get("market_regime") or {}
|
||||||
|
if entry_plan:
|
||||||
|
entry_plan, market_risk_gate_reason = _apply_market_risk_entry_gate(entry_plan, signals, market_regime)
|
||||||
|
if market_risk_gate_reason:
|
||||||
|
score += factor_scorer.delta(
|
||||||
|
"entry_quality_gate",
|
||||||
|
-2,
|
||||||
|
evidence="全局市场风险闸门降为观察",
|
||||||
|
value=market_risk_gate_reason,
|
||||||
|
)
|
||||||
factor_score_breakdown = factor_scorer.summary()
|
factor_score_breakdown = factor_scorer.summary()
|
||||||
opportunity_score = round(float(factor_score_breakdown.get("opportunity_score") or 0), 3)
|
opportunity_score = round(float(factor_score_breakdown.get("opportunity_score") or 0), 3)
|
||||||
entry_score = round(float(factor_score_breakdown.get("entry_score") or 0), 3)
|
entry_score = round(float(factor_score_breakdown.get("entry_score") or 0), 3)
|
||||||
@ -1498,7 +1533,7 @@ def confirm_burst(symbol, cand):
|
|||||||
"pa_15min": pa_15min_result,
|
"pa_15min": pa_15min_result,
|
||||||
"pa_1d": pa_1d,
|
"pa_1d": pa_1d,
|
||||||
"m30_aligned": m30_aligned,
|
"m30_aligned": m30_aligned,
|
||||||
"entry_action": entry_action,
|
"entry_action": (entry_plan or {}).get("entry_action") or entry_action,
|
||||||
"market_context": market_context,
|
"market_context": market_context,
|
||||||
"derivatives_context": derivatives_context,
|
"derivatives_context": derivatives_context,
|
||||||
"sector_context": sector_context,
|
"sector_context": sector_context,
|
||||||
@ -1584,6 +1619,15 @@ def main(compact: bool = False):
|
|||||||
try:
|
try:
|
||||||
init_db()
|
init_db()
|
||||||
expire_old_states()
|
expire_old_states()
|
||||||
|
regime_context = _current_market_regime_context()
|
||||||
|
market_regime = regime_context.get("market_regime") or {}
|
||||||
|
if str(market_regime.get("risk_level") or "").strip().lower() == "critical" and float(market_regime.get("position_multiplier") or 0) <= 0:
|
||||||
|
downgrade_result = downgrade_active_entries_for_market_risk(
|
||||||
|
"全市场处于 critical 风险,暂停新开仓与新挂单,保留为观察机会",
|
||||||
|
event_time=datetime.now().isoformat(),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
downgrade_result = {"updated_count": 0}
|
||||||
|
|
||||||
candidates = get_candidates_for_confirm()
|
candidates = get_candidates_for_confirm()
|
||||||
|
|
||||||
@ -1591,6 +1635,7 @@ def main(compact: bool = False):
|
|||||||
output = {
|
output = {
|
||||||
"status": "no_candidates",
|
"status": "no_candidates",
|
||||||
"message": "无需要确认的候选(需加速状态+评分≥6)",
|
"message": "无需要确认的候选(需加速状态+评分≥6)",
|
||||||
|
"market_risk_downgraded_count": downgrade_result.get("updated_count", 0),
|
||||||
"check_time": datetime.now().isoformat(),
|
"check_time": datetime.now().isoformat(),
|
||||||
}
|
}
|
||||||
_emit_output(output, compact=compact)
|
_emit_output(output, compact=compact)
|
||||||
@ -1734,6 +1779,7 @@ def main(compact: bool = False):
|
|||||||
"status": "confirmed" if confirmed else "unconfirmed",
|
"status": "confirmed" if confirmed else "unconfirmed",
|
||||||
"confirmed_count": len(confirmed),
|
"confirmed_count": len(confirmed),
|
||||||
"unconfirmed_count": len(unconfirmed),
|
"unconfirmed_count": len(unconfirmed),
|
||||||
|
"market_risk_downgraded_count": downgrade_result.get("updated_count", 0),
|
||||||
"confirmed": confirmed,
|
"confirmed": confirmed,
|
||||||
"unconfirmed": unconfirmed,
|
"unconfirmed": unconfirmed,
|
||||||
"check_time": datetime.now().isoformat(),
|
"check_time": datetime.now().isoformat(),
|
||||||
|
|||||||
@ -8,7 +8,7 @@ from datetime import datetime
|
|||||||
import ccxt
|
import ccxt
|
||||||
|
|
||||||
from app.db.altcoin_db import init_db, log_cron_run, update_latest_price_cache
|
from app.db.altcoin_db import init_db, log_cron_run, update_latest_price_cache
|
||||||
from app.db.paper_trading import get_paper_trading_summary, sync_recommendation
|
from app.db.paper_trading import get_paper_trading_summary, sync_pending_paper_orders, sync_recommendation
|
||||||
from app.db.recommendation_queries import get_active_recommendations_deduped
|
from app.db.recommendation_queries import get_active_recommendations_deduped
|
||||||
from app.services.live_trading_sync import sync_paper_trade_to_live
|
from app.services.live_trading_sync import sync_paper_trade_to_live
|
||||||
|
|
||||||
@ -35,12 +35,19 @@ def run_once(limit: int = 100) -> dict:
|
|||||||
results.append(result)
|
results.append(result)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
failed.append({"symbol": symbol, "error": str(exc)})
|
failed.append({"symbol": symbol, "error": str(exc)})
|
||||||
|
pending_result = sync_pending_paper_orders(limit=limit, event_time=datetime.now().isoformat())
|
||||||
|
for item in pending_result.get("results", []):
|
||||||
|
if item.get("trade_id") and (item.get("opened") or item.get("paper_order", {}).get("filled")):
|
||||||
|
item["live_sync"] = sync_paper_trade_to_live(int(item["trade_id"]), execute=True)
|
||||||
output = {
|
output = {
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"processed_count": len(results),
|
"processed_count": len(results),
|
||||||
|
"pending_processed_count": pending_result.get("processed_count", 0),
|
||||||
|
"pending_filled_count": pending_result.get("filled_count", 0),
|
||||||
"failed_count": len(failed),
|
"failed_count": len(failed),
|
||||||
"failed": failed,
|
"failed": failed,
|
||||||
"results": results,
|
"results": results,
|
||||||
|
"pending_results": pending_result.get("results", []),
|
||||||
"summary": get_paper_trading_summary(days=30),
|
"summary": get_paper_trading_summary(days=30),
|
||||||
"run_time": datetime.now().isoformat(),
|
"run_time": datetime.now().isoformat(),
|
||||||
}
|
}
|
||||||
@ -77,6 +84,8 @@ def main(limit: int = 100):
|
|||||||
duration_ms=int((finished_at - started_at).total_seconds() * 1000),
|
duration_ms=int((finished_at - started_at).total_seconds() * 1000),
|
||||||
summary={
|
summary={
|
||||||
"processed_count": output.get("processed_count", 0),
|
"processed_count": output.get("processed_count", 0),
|
||||||
|
"pending_processed_count": output.get("pending_processed_count", 0),
|
||||||
|
"pending_filled_count": output.get("pending_filled_count", 0),
|
||||||
"failed_count": output.get("failed_count", 0),
|
"failed_count": output.get("failed_count", 0),
|
||||||
"open_count": output.get("summary", {}).get("open_count", 0),
|
"open_count": output.get("summary", {}).get("open_count", 0),
|
||||||
"closed_count": output.get("summary", {}).get("closed_count", 0),
|
"closed_count": output.get("summary", {}).get("closed_count", 0),
|
||||||
|
|||||||
@ -87,10 +87,10 @@ def _get_thresholds():
|
|||||||
"hit_threshold_pct": params.get("hit_threshold_pct", 5.0),
|
"hit_threshold_pct": params.get("hit_threshold_pct", 5.0),
|
||||||
"fail_threshold_pct": params.get("fail_threshold_pct", -3.0),
|
"fail_threshold_pct": params.get("fail_threshold_pct", -3.0),
|
||||||
"missed_explosion_pct": params.get("missed_explosion_pct", 20.0),
|
"missed_explosion_pct": params.get("missed_explosion_pct", 20.0),
|
||||||
"min_samples_for_weight": params.get("min_samples_for_weight", 3),
|
"min_samples_for_weight": params.get("min_samples_for_weight", 12),
|
||||||
"weight_floor": params.get("weight_floor", 0.0),
|
"weight_floor": params.get("weight_floor", 0.0),
|
||||||
"hit_rate_kill_threshold": params.get("hit_rate_kill_threshold", 0.10),
|
"hit_rate_kill_threshold": params.get("hit_rate_kill_threshold", 0.10),
|
||||||
"kill_min_samples": params.get("kill_min_samples", 5),
|
"kill_min_samples": params.get("kill_min_samples", 20),
|
||||||
"category_base_weights": params.get("category_base_weights", {"前瞻": 2.0, "PA": 1.5, "滞后": 0.5}),
|
"category_base_weights": params.get("category_base_weights", {"前瞻": 2.0, "PA": 1.5, "滞后": 0.5}),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -605,8 +605,8 @@ def _apply_daily_factor_weight_governance():
|
|||||||
"""
|
"""
|
||||||
thresholds = _get_thresholds()
|
thresholds = _get_thresholds()
|
||||||
weights = get_signal_weights()
|
weights = get_signal_weights()
|
||||||
min_samples = max(3, int(thresholds.get("min_samples_for_weight", 3) or 3))
|
min_samples = max(12, int(thresholds.get("min_samples_for_weight", 12) or 12))
|
||||||
kill_min_samples = max(min_samples, int(thresholds.get("kill_min_samples", 5) or 5))
|
kill_min_samples = max(min_samples, int(thresholds.get("kill_min_samples", 20) or 20))
|
||||||
kill_hit_rate = float(thresholds.get("hit_rate_kill_threshold", 0.10) or 0.10) * 100
|
kill_hit_rate = float(thresholds.get("hit_rate_kill_threshold", 0.10) or 0.10) * 100
|
||||||
warn_hit_rate = float((thresholds.get("signal_deprecation") or {}).get("hit_rate_warn_threshold", 0.20) or 0.20) * 100
|
warn_hit_rate = float((thresholds.get("signal_deprecation") or {}).get("hit_rate_warn_threshold", 0.20) or 0.20) * 100
|
||||||
category_base = thresholds.get("category_base_weights") or {"前瞻": 2.0, "PA": 1.5, "滞后": 0.5}
|
category_base = thresholds.get("category_base_weights") or {"前瞻": 2.0, "PA": 1.5, "滞后": 0.5}
|
||||||
|
|||||||
@ -271,21 +271,21 @@ review:
|
|||||||
hit_threshold_pct: 5.0
|
hit_threshold_pct: 5.0
|
||||||
fail_threshold_pct: -3.0
|
fail_threshold_pct: -3.0
|
||||||
missed_explosion_pct: 20.0
|
missed_explosion_pct: 20.0
|
||||||
min_samples_for_weight: 3
|
min_samples_for_weight: 12
|
||||||
weight_floor: 0.0
|
weight_floor: 0.0
|
||||||
hit_rate_kill_threshold: 0.1
|
hit_rate_kill_threshold: 0.1
|
||||||
kill_min_samples: 5
|
kill_min_samples: 20
|
||||||
category_base_weights:
|
category_base_weights:
|
||||||
前瞻: 2.0
|
前瞻: 2.0
|
||||||
PA: 1.5
|
PA: 1.5
|
||||||
滞后: 0.5
|
滞后: 0.5
|
||||||
signal_deprecation:
|
signal_deprecation:
|
||||||
enabled: true
|
enabled: true
|
||||||
min_samples: 10
|
min_samples: 20
|
||||||
hit_rate_warn_threshold: 0.2
|
hit_rate_warn_threshold: 0.2
|
||||||
hit_rate_deprecate_threshold: 0.1
|
hit_rate_deprecate_threshold: 0.1
|
||||||
min_tracking_days: 3
|
min_tracking_days: 3
|
||||||
note: 信号淘汰机制。累计≥10样本+跟踪≥3天后,命中率<10%自动淘汰(权重→0);<20%警告并自动降权。
|
note: 信号淘汰机制。累计≥20样本+跟踪≥3天后,命中率<10%自动淘汰(权重→0);<20%警告并自动降权;小样本只观察,不直接影响确认层权重。
|
||||||
deprecate_action: set_weight_zero
|
deprecate_action: set_weight_zero
|
||||||
warn_action: halve_weight
|
warn_action: halve_weight
|
||||||
reverse_analysis:
|
reverse_analysis:
|
||||||
|
|||||||
28
tests/test_confirm_market_risk_gate.py
Normal file
28
tests/test_confirm_market_risk_gate.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
from app.services.altcoin_confirm import _apply_market_risk_entry_gate
|
||||||
|
|
||||||
|
|
||||||
|
def test_market_risk_gate_blocks_executable_action_when_critical():
|
||||||
|
signals = ["15min即刻入场信号"]
|
||||||
|
plan, reason = _apply_market_risk_entry_gate(
|
||||||
|
{"entry_action": "可即刻买入", "entry_price": 1.0},
|
||||||
|
signals,
|
||||||
|
{"risk_level": "critical", "position_multiplier": 0.0},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert plan["entry_action"] == "观察"
|
||||||
|
assert plan["market_risk_gate"]["blocked_action"] == "可即刻买入"
|
||||||
|
assert "暂停新开仓" in reason
|
||||||
|
assert any("市场风控闸门" in sig for sig in signals)
|
||||||
|
|
||||||
|
|
||||||
|
def test_market_risk_gate_keeps_observation_action_visible():
|
||||||
|
signals = []
|
||||||
|
plan, reason = _apply_market_risk_entry_gate(
|
||||||
|
{"entry_action": "观察", "entry_price": 1.0},
|
||||||
|
signals,
|
||||||
|
{"risk_level": "critical", "position_multiplier": 0.0},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert plan["entry_action"] == "观察"
|
||||||
|
assert "market_risk_gate" not in plan
|
||||||
|
assert reason == ""
|
||||||
@ -45,10 +45,41 @@ def test_factor_scorer_records_negative_risk_as_risk_score():
|
|||||||
|
|
||||||
|
|
||||||
def test_signal_weight_alias_keeps_legacy_chinese_keys_available(monkeypatch):
|
def test_signal_weight_alias_keeps_legacy_chinese_keys_available(monkeypatch):
|
||||||
monkeypatch.setattr("app.config.config_loader.load_rules", lambda: {"signal_weights": {"量价齐飞": 5}})
|
monkeypatch.setattr("app.config.config_loader._load_yaml_baseline", lambda: {"signal_weights": {"量价齐飞": 5}})
|
||||||
monkeypatch.setattr("app.db.altcoin_db.get_signal_weights", lambda: {})
|
monkeypatch.setattr("app.db.altcoin_db.get_signal_weights", lambda: {})
|
||||||
|
|
||||||
weights = get_signal_weights()
|
weights = get_signal_weights()
|
||||||
|
|
||||||
assert weights["vp_fly_1h_current"] == 5
|
assert weights["vp_fly_1h_current"] == 5
|
||||||
assert weights["量价齐飞"] == 5
|
assert weights["量价齐飞"] == 5
|
||||||
|
|
||||||
|
|
||||||
|
def test_signal_weight_ignores_thin_dynamic_samples(monkeypatch):
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"app.config.config_loader._load_yaml_baseline",
|
||||||
|
lambda: {"signal_weights": {"量价齐飞": 5}},
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"app.config.config_loader.load_rules",
|
||||||
|
lambda: {
|
||||||
|
"review": {
|
||||||
|
"min_samples_for_weight": 12,
|
||||||
|
"signal_deprecation": {"min_samples": 20},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"app.db.altcoin_db.get_signal_weights",
|
||||||
|
lambda: {
|
||||||
|
"vp_fly_1h_current": {
|
||||||
|
"weight": 0,
|
||||||
|
"total_count": 3,
|
||||||
|
"hit_rate": 0,
|
||||||
|
"avg_pnl": -1,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
weights = get_signal_weights()
|
||||||
|
|
||||||
|
assert weights["vp_fly_1h_current"] == 5
|
||||||
|
|||||||
39
tests/test_market_risk_recommendation_downgrade.py
Normal file
39
tests/test_market_risk_recommendation_downgrade.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
from app.db.recommendation_commands import downgrade_active_entries_for_market_risk
|
||||||
|
|
||||||
|
|
||||||
|
def test_downgrade_active_entries_for_market_risk_updates_executable_recs(pg_conn):
|
||||||
|
pg_conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO recommendation (
|
||||||
|
symbol, rec_time, rec_state, rec_score, entry_price, status,
|
||||||
|
action_status, execution_status, display_bucket, lifecycle_state,
|
||||||
|
entry_plan_json
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
('BUY/USDT', '2026-05-26T10:00:00', '爆发', 30, 1.0, 'active',
|
||||||
|
'可即刻买入', 'buy_now', 'realtime', 'buyable', '{"entry_action":"可即刻买入"}'),
|
||||||
|
('WAIT/USDT', '2026-05-26T10:01:00', '爆发', 20, 1.0, 'active',
|
||||||
|
'等回踩', 'wait_pullback', 'watch_pool', 'waiting_entry', '{"entry_action":"等回踩"}'),
|
||||||
|
('OBS/USDT', '2026-05-26T10:02:00', '观察', 10, 1.0, 'active',
|
||||||
|
'观察', 'observe', 'watch_pool', 'watching', '{"entry_action":"观察"}')
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
pg_conn.commit()
|
||||||
|
|
||||||
|
result = downgrade_active_entries_for_market_risk("critical test", event_time="2026-05-26T11:00:00")
|
||||||
|
|
||||||
|
assert result["updated_count"] == 2
|
||||||
|
rows = pg_conn.execute(
|
||||||
|
"SELECT symbol, action_status, execution_status, display_bucket, entry_plan_json FROM recommendation ORDER BY symbol"
|
||||||
|
).fetchall()
|
||||||
|
by_symbol = {row["symbol"]: dict(row) for row in rows}
|
||||||
|
assert by_symbol["BUY/USDT"]["action_status"] == "观察"
|
||||||
|
assert by_symbol["BUY/USDT"]["execution_status"] == "observe"
|
||||||
|
assert by_symbol["WAIT/USDT"]["action_status"] == "观察"
|
||||||
|
assert by_symbol["WAIT/USDT"]["execution_status"] == "observe"
|
||||||
|
assert by_symbol["OBS/USDT"]["action_status"] == "观察"
|
||||||
|
plan = json.loads(by_symbol["BUY/USDT"]["entry_plan_json"])
|
||||||
|
assert plan["market_risk_gate"]["blocked_action"] == "可即刻买入"
|
||||||
|
assert plan["market_risk_gate"]["final_action"] == "观察"
|
||||||
@ -14,6 +14,7 @@ from app.db.paper_trading import (
|
|||||||
list_paper_trades,
|
list_paper_trades,
|
||||||
reset_paper_trading_data,
|
reset_paper_trading_data,
|
||||||
send_paper_trading_report,
|
send_paper_trading_report,
|
||||||
|
sync_pending_paper_orders,
|
||||||
sync_recommendation,
|
sync_recommendation,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -675,6 +676,47 @@ def test_wait_pullback_paper_order_fills_when_price_touches(monkeypatch):
|
|||||||
assert order["fill_price"] == pytest.approx(95)
|
assert order["fill_price"] == pytest.approx(95)
|
||||||
|
|
||||||
|
|
||||||
|
def test_pending_paper_order_reconciles_from_latest_price_cache(monkeypatch):
|
||||||
|
monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "1")
|
||||||
|
monkeypatch.setenv("ALPHAX_PAPER_TRADE_NOTIONAL_USDT", "100")
|
||||||
|
monkeypatch.setenv("ALPHAX_PAPER_TRADE_FEE_RATE", "0")
|
||||||
|
monkeypatch.setenv("ALPHAX_PAPER_TRADE_SLIPPAGE_PCT", "0")
|
||||||
|
altcoin_db.init_db()
|
||||||
|
rec_id = altcoin_db.create_recommendation(
|
||||||
|
symbol="CACHEFILL/USDT",
|
||||||
|
rec_state="蓄力",
|
||||||
|
rec_score=22,
|
||||||
|
entry_price=95,
|
||||||
|
stop_loss=90,
|
||||||
|
tp1=105,
|
||||||
|
tp2=112,
|
||||||
|
signals=["等待回踩"],
|
||||||
|
entry_plan={"entry_action": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "risk_reward_ok": True, "rr1": 2.0},
|
||||||
|
)
|
||||||
|
rec = {
|
||||||
|
"id": rec_id,
|
||||||
|
"symbol": "CACHEFILL/USDT",
|
||||||
|
"execution_status": "wait_pullback",
|
||||||
|
"action_status": "等回踩",
|
||||||
|
"entry_price": 95,
|
||||||
|
"stop_loss": 90,
|
||||||
|
"tp1": 105,
|
||||||
|
"tp2": 112,
|
||||||
|
"entry_plan": {"entry_action": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "risk_reward_ok": True, "rr1": 2.0},
|
||||||
|
}
|
||||||
|
sync_recommendation(rec, 100, event_time="2026-05-16T10:00:00")
|
||||||
|
altcoin_db.update_latest_price_cache("CACHEFILL/USDT", 94.9, updated_at="2026-05-16T10:05:00", source="test")
|
||||||
|
|
||||||
|
result = sync_pending_paper_orders(event_time="2026-05-16T10:05:00")
|
||||||
|
|
||||||
|
assert result["filled_count"] == 1
|
||||||
|
assert result["results"][0]["paper_order"]["filled"] is True
|
||||||
|
trade = list_paper_trades()["items"][0]
|
||||||
|
assert trade["symbol"] == "CACHEFILL/USDT"
|
||||||
|
order = list_paper_orders(status="filled")["items"][0]
|
||||||
|
assert order["fill_price"] == pytest.approx(95)
|
||||||
|
|
||||||
|
|
||||||
def test_wait_pullback_order_cancels_when_recommendation_invalid(monkeypatch):
|
def test_wait_pullback_order_cancels_when_recommendation_invalid(monkeypatch):
|
||||||
monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "1")
|
monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "1")
|
||||||
altcoin_db.init_db()
|
altcoin_db.init_db()
|
||||||
|
|||||||
@ -6,15 +6,29 @@ from app.db.runtime_config_db import set_config
|
|||||||
from app.services import price_streamer
|
from app.services import price_streamer
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
def _paper_config(**overrides):
|
||||||
def buy_now_rec(monkeypatch):
|
cfg = {
|
||||||
set_config("system", "paper_trading", {
|
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"trade_notional_usdt": 5000,
|
"trade_notional_usdt": 5000,
|
||||||
"trade_leverage": 5,
|
"trade_leverage": 5,
|
||||||
"fee_rate": 0,
|
"fee_rate": 0,
|
||||||
"slippage_pct": 0,
|
"slippage_pct": 0,
|
||||||
})
|
"entry_gate_enabled": False,
|
||||||
|
"entry_min_rr": 1.2,
|
||||||
|
"order_min_rr": 1.2,
|
||||||
|
"max_cumulative_leverage": 0,
|
||||||
|
"max_stop_loss_leverage_risk_pct": 0,
|
||||||
|
"max_account_drawdown_pause_pct": 0,
|
||||||
|
"pause_after_weak_entries": 0,
|
||||||
|
"global_risk_gate_enabled": False,
|
||||||
|
}
|
||||||
|
cfg.update(overrides)
|
||||||
|
return cfg
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def buy_now_rec(monkeypatch):
|
||||||
|
set_config("system", "paper_trading", _paper_config())
|
||||||
set_config("system", "price_streamer", {
|
set_config("system", "price_streamer", {
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"update_latest_price_cache": True,
|
"update_latest_price_cache": True,
|
||||||
@ -95,13 +109,7 @@ def test_price_streamer_tracks_open_paper_trade_without_active_rec(buy_now_rec):
|
|||||||
|
|
||||||
|
|
||||||
def test_price_streamer_fills_pending_paper_order():
|
def test_price_streamer_fills_pending_paper_order():
|
||||||
set_config("system", "paper_trading", {
|
set_config("system", "paper_trading", _paper_config())
|
||||||
"enabled": True,
|
|
||||||
"trade_notional_usdt": 5000,
|
|
||||||
"trade_leverage": 5,
|
|
||||||
"fee_rate": 0,
|
|
||||||
"slippage_pct": 0,
|
|
||||||
})
|
|
||||||
set_config("system", "price_streamer", {
|
set_config("system", "price_streamer", {
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"update_latest_price_cache": True,
|
"update_latest_price_cache": True,
|
||||||
@ -140,13 +148,7 @@ def test_price_streamer_fills_pending_paper_order():
|
|||||||
|
|
||||||
|
|
||||||
def test_price_streamer_prioritizes_pending_order_over_same_symbol_recommendation():
|
def test_price_streamer_prioritizes_pending_order_over_same_symbol_recommendation():
|
||||||
set_config("system", "paper_trading", {
|
set_config("system", "paper_trading", _paper_config())
|
||||||
"enabled": True,
|
|
||||||
"trade_notional_usdt": 5000,
|
|
||||||
"trade_leverage": 5,
|
|
||||||
"fee_rate": 0,
|
|
||||||
"slippage_pct": 0,
|
|
||||||
})
|
|
||||||
set_config("system", "price_streamer", {
|
set_config("system", "price_streamer", {
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"update_latest_price_cache": True,
|
"update_latest_price_cache": True,
|
||||||
|
|||||||
@ -161,7 +161,7 @@ def test_daily_factor_weight_governance_promotes_and_eliminates(monkeypatch, tem
|
|||||||
monkeypatch.setattr(review_engine, "update_signal_weight", lambda signal, weight: changes.append((signal, weight)))
|
monkeypatch.setattr(review_engine, "update_signal_weight", lambda signal, weight: changes.append((signal, weight)))
|
||||||
monkeypatch.setattr(review_engine, "get_signal_weights", lambda: {
|
monkeypatch.setattr(review_engine, "get_signal_weights", lambda: {
|
||||||
"good_factor": {"category": "前瞻", "total_count": 12, "hit_rate": 70, "avg_pnl": 4.2, "weight": 1.0},
|
"good_factor": {"category": "前瞻", "total_count": 12, "hit_rate": 70, "avg_pnl": 4.2, "weight": 1.0},
|
||||||
"bad_factor": {"category": "前瞻", "total_count": 12, "hit_rate": 5, "avg_pnl": -2.0, "weight": 1.2},
|
"bad_factor": {"category": "前瞻", "total_count": 20, "hit_rate": 5, "avg_pnl": -2.0, "weight": 1.2},
|
||||||
"thin_factor": {"category": "PA", "total_count": 2, "hit_rate": 100, "avg_pnl": 6.0, "weight": 1.0},
|
"thin_factor": {"category": "PA", "total_count": 2, "hit_rate": 100, "avg_pnl": 6.0, "weight": 1.0},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user