This commit is contained in:
aaron 2026-05-21 20:27:50 +08:00
parent cabd96a875
commit f3463e284e
8 changed files with 273 additions and 36 deletions

View File

@ -62,6 +62,18 @@ ALPHAX_ONCHAIN_CANDIDATE_MIN_SCORE=70
ALPHAX_ONCHAIN_CANDIDATE_MIN_CONFIDENCE=70
ALPHAX_ONCHAIN_CANDIDATE_COOLDOWN_HOURS=6
ALPHAX_ONCHAIN_WHALE_TX_USD=250000
# 策略交易挂单门控。wait_pullback 只是候选,必须通过这些条件才会创建挂单。
ALPHAX_PAPER_ORDER_GATE_ENABLED=1
ALPHAX_PAPER_MAX_CUMULATIVE_LEVERAGE=5
ALPHAX_PAPER_ORDER_MIN_REC_SCORE=20
ALPHAX_PAPER_ORDER_MIN_RR=1.2
ALPHAX_PAPER_ORDER_REQUIRE_RISK_REWARD_OK=1
ALPHAX_PAPER_ORDER_MAX_DISTANCE_TO_ENTRY_PCT=8
ALPHAX_PAPER_ORDER_REQUIRE_CURRENT_TRIGGER=0
ALPHAX_PAPER_ORDER_CANCEL_FAR_FROM_ENTRY_PCT=12
ALPHAX_PAPER_ORDER_EXPIRE_HOURS=24
ALPHAX_SYSTEM_ERROR_FEISHU_ENABLED=0
ALPHAX_SYSTEM_ERROR_FEISHU_WEBHOOK=

View File

@ -100,6 +100,7 @@ def default_paper_trading_config():
"account_equity_usdt": _env_float("ALPHAX_PAPER_ACCOUNT_EQUITY_USDT", 20000),
"trade_notional_usdt": _env_float("ALPHAX_PAPER_TRADE_NOTIONAL_USDT", 5000),
"trade_leverage": _env_float("ALPHAX_PAPER_TRADE_LEVERAGE", 5),
"max_cumulative_leverage": _env_float("ALPHAX_PAPER_MAX_CUMULATIVE_LEVERAGE", 5.0),
"fee_rate": _env_float("ALPHAX_PAPER_TRADE_FEE_RATE", 0.001),
"slippage_pct": _env_float("ALPHAX_PAPER_TRADE_SLIPPAGE_PCT", 0.05),
"trailing_stop_enabled": _env_bool("ALPHAX_PAPER_TRAILING_STOP_ENABLED", True),
@ -109,7 +110,9 @@ def default_paper_trading_config():
"trailing_move_push_min_interval_seconds": _env_int("ALPHAX_PAPER_TRAILING_MOVE_PUSH_MIN_INTERVAL_SECONDS", 300),
"trailing_move_push_min_step_pct": _env_float("ALPHAX_PAPER_TRAILING_MOVE_PUSH_MIN_STEP_PCT", 2.0),
"order_gate_enabled": _env_bool("ALPHAX_PAPER_ORDER_GATE_ENABLED", True),
"order_min_rec_score": _env_float("ALPHAX_PAPER_ORDER_MIN_REC_SCORE", 20.0),
"order_min_rr": _env_float("ALPHAX_PAPER_ORDER_MIN_RR", 1.2),
"order_require_risk_reward_ok": _env_bool("ALPHAX_PAPER_ORDER_REQUIRE_RISK_REWARD_OK", True),
"order_max_distance_to_entry_pct": _env_float("ALPHAX_PAPER_ORDER_MAX_DISTANCE_TO_ENTRY_PCT", 8.0),
"order_require_current_trigger": _env_bool("ALPHAX_PAPER_ORDER_REQUIRE_CURRENT_TRIGGER", False),
"order_cancel_far_from_entry_pct": _env_float("ALPHAX_PAPER_ORDER_CANCEL_FAR_FROM_ENTRY_PCT", 12.0),

View File

@ -65,6 +65,43 @@ def default_slippage_pct(config: dict | None = None) -> float:
return max(0.0, _safe_float(_paper_cfg(config).get("slippage_pct"), 0.05))
def max_cumulative_leverage(config: dict | None = None) -> float:
return max(0.0, _safe_float(_paper_cfg(config).get("max_cumulative_leverage"), 5.0))
def _cumulative_leverage_check(conn, additional_notional: float, config: dict | None = None, exclude_rec_id: int = 0) -> tuple[bool, dict]:
cfg = _paper_cfg(config)
equity = default_account_equity_usdt(cfg)
cap = max_cumulative_leverage(cfg)
if cap <= 0:
return True, {"max_cumulative_leverage": cap, "disabled": True}
exclude_rec_id = _safe_int(exclude_rec_id)
open_params = []
pending_params = []
open_where = "status='open'"
pending_where = "status='pending'"
if exclude_rec_id > 0:
open_where += " AND recommendation_id<>%s"
pending_where += " AND recommendation_id<>%s"
open_params.append(exclude_rec_id)
pending_params.append(exclude_rec_id)
open_notional = _safe_float(conn.execute(f"SELECT COALESCE(SUM(notional_usdt),0) FROM paper_trades WHERE {open_where}", tuple(open_params)).fetchone()[0])
pending_notional = _safe_float(conn.execute(f"SELECT COALESCE(SUM(notional_usdt),0) FROM paper_orders WHERE {pending_where}", tuple(pending_params)).fetchone()[0])
add = max(0.0, _safe_float(additional_notional))
projected_notional = open_notional + pending_notional + add
projected_leverage = projected_notional / equity if equity > 0 else 0
detail = {
"account_equity_usdt": equity,
"open_notional_usdt": round(open_notional, 8),
"pending_notional_usdt": round(pending_notional, 8),
"additional_notional_usdt": round(add, 8),
"projected_notional_usdt": round(projected_notional, 8),
"projected_cumulative_leverage": round(projected_leverage, 6),
"max_cumulative_leverage": cap,
}
return projected_leverage <= cap + 1e-12, detail
def _trailing_config() -> dict:
cfg = paper_trading_config()
return {
@ -404,6 +441,14 @@ def _open_trade(conn, rec: dict, current_price: float, event_time: str, config:
plan = _entry_plan(rec)
entry_price = _open_price(current_price, cfg)
notional = default_notional_usdt(cfg)
leverage_ok, leverage_detail = _cumulative_leverage_check(conn, notional, cfg, exclude_rec_id=rec_id)
if not leverage_ok:
return {
"opened": False,
"skipped": True,
"reason": "cumulative_leverage_exceeded",
"risk_detail": leverage_detail,
}
leverage = default_leverage(cfg)
margin = default_margin_usdt(cfg)
qty = round(notional / entry_price, 12) if entry_price > 0 else 0
@ -553,7 +598,7 @@ def _paper_order_distance_pct(side: str, current_price: float, target: float) ->
return max(0.0, (current_price / target - 1) * 100)
def _paper_order_gate(rec: dict, current_price: float, config: dict | None = None) -> tuple[bool, list[str], dict]:
def _paper_order_gate(rec: dict, current_price: float, config: dict | None = None, conn=None) -> tuple[bool, list[str], dict]:
cfg = _paper_cfg(config)
if not bool(cfg.get("order_gate_enabled", True)):
return True, [], {"gate_enabled": False}
@ -567,6 +612,20 @@ def _paper_order_gate(rec: dict, current_price: float, config: dict | None = Non
calc_rr = _paper_order_rr(side, target, stop_loss, tp1)
effective_rr = rr if rr > 0 else calc_rr
min_rr = max(0.0, _safe_float(cfg.get("order_min_rr"), 1.2))
min_rec_score = max(0.0, _safe_float(cfg.get("order_min_rec_score"), 20.0))
rec_score = _safe_float(rec.get("rec_score") or rec.get("score"))
if rec_score <= 0 and conn is not None and _safe_int(rec.get("id")) > 0:
row = conn.execute("SELECT rec_score FROM recommendation WHERE id=%s", (_safe_int(rec.get("id")),)).fetchone()
rec_score = _safe_float(row["rec_score"] if row else 0)
leverage_ok = True
leverage_detail = {}
if conn is not None:
leverage_ok, leverage_detail = _cumulative_leverage_check(
conn,
default_notional_usdt(cfg),
cfg,
exclude_rec_id=_safe_int(rec.get("id")),
)
distance_pct = _paper_order_distance_pct(side, current_price, target)
max_distance = max(0.0, _safe_float(cfg.get("order_max_distance_to_entry_pct"), 8.0))
opportunity_level = str(plan.get("opportunity_level") or rec.get("opportunity_level") or "").strip()
@ -585,6 +644,12 @@ def _paper_order_gate(rec: dict, current_price: float, config: dict | None = Non
reasons.append("invalid_risk_geometry")
if risk_reward_ok is False:
reasons.append("risk_reward_rejected")
if bool(cfg.get("order_require_risk_reward_ok", True)) and risk_reward_ok is not True:
reasons.append("risk_reward_not_confirmed")
if rec_score < min_rec_score:
reasons.append("rec_score_below_min")
if not leverage_ok:
reasons.append("cumulative_leverage_exceeded")
if effective_rr > 0 and effective_rr < min_rr:
reasons.append("rr_below_min")
if effective_rr <= 0:
@ -605,6 +670,9 @@ def _paper_order_gate(rec: dict, current_price: float, config: dict | None = Non
"distance_to_entry_pct": round(distance_pct, 4),
"max_distance_to_entry_pct": max_distance,
"min_rr": min_rr,
"rec_score": rec_score,
"min_rec_score": min_rec_score,
"leverage": leverage_detail,
"opportunity_level": opportunity_level,
"entry_trigger_confirmed": trigger_ok,
}
@ -759,7 +827,7 @@ def _sync_wait_pullback_order(conn, rec: dict, current_price: float, event_time:
"current_price": current_price,
}
gate_ok, gate_reasons, gate_detail = _paper_order_gate(rec, current_price, cfg)
gate_ok, gate_reasons, gate_detail = _paper_order_gate(rec, current_price, cfg, conn=conn)
if not gate_ok:
return {
"skipped": True,
@ -1008,6 +1076,22 @@ def sync_recommendation(rec: dict, current_price: float, event_time: str = "") -
if trade:
trade = dict(trade)
if trade.get("status") == "open":
conn.execute(
"""
UPDATE paper_orders
SET status='filled',
fill_price=%s,
filled_at=COALESCE(NULLIF(filled_at,''), %s),
updated_at=%s
WHERE recommendation_id=%s AND status='pending'
""",
(
_safe_float(trade.get("entry_price")),
trade.get("opened_at") or event_time,
event_time,
rec_id,
),
)
result = _update_open_trade(conn, trade, current_price, event_time)
conn.commit()
return result

View File

@ -39,6 +39,10 @@ def _fingerprint(error_type: str, message: str, stack_trace: str, path: str = ""
return hashlib.sha256(basis.encode("utf-8", errors="ignore")).hexdigest()[:32]
def _should_push_alert(level: str) -> bool:
return str(level or "").strip().lower() in {"error", "critical", "fatal"}
def record_system_error(
*,
source: str,
@ -92,20 +96,21 @@ def record_system_error(
)
log_id = row.fetchone()["id"]
conn.commit()
push_system_error_alert({
"id": int(log_id),
"created_at": _now(),
"level": _truncate(level or "error", 20),
"source": _truncate(source or "app", 80),
"error_type": _truncate(error_type, 160),
"message": _truncate(message, 2000),
"stack_trace": _truncate(stack_trace, 60000),
"request_method": _truncate(request_method, 16),
"request_path": _truncate(request_path, 500),
"user_email": _truncate(user_email, 255),
"status_code": int(status_code or 0),
"fingerprint": fingerprint,
})
if _should_push_alert(level or "error"):
push_system_error_alert({
"id": int(log_id),
"created_at": _now(),
"level": _truncate(level or "error", 20),
"source": _truncate(source or "app", 80),
"error_type": _truncate(error_type, 160),
"message": _truncate(message, 2000),
"stack_trace": _truncate(stack_trace, 60000),
"request_method": _truncate(request_method, 16),
"request_path": _truncate(request_path, 500),
"user_email": _truncate(user_email, 255),
"status_code": int(status_code or 0),
"fingerprint": fingerprint,
})
return int(log_id)
finally:
conn.close()

View File

@ -121,15 +121,15 @@ def load_stream_targets(limit: int | None = None, cfg: dict | None = None) -> di
targets[symbol] = dict(rec)
if cfg.get("include_open_paper_trades", True):
for rec in _load_open_paper_trade_recs():
symbol = str(rec.get("symbol") or "").strip().upper()
if symbol:
targets.setdefault(symbol, rec)
for rec in _load_pending_paper_order_recs():
symbol = str(rec.get("symbol") or "").strip().upper()
if symbol:
targets.setdefault(symbol, rec)
targets[symbol] = rec
for rec in _load_open_paper_trade_recs():
symbol = str(rec.get("symbol") or "").strip().upper()
if symbol:
targets[symbol] = rec
return dict(list(targets.items())[:max_symbols])

View File

@ -244,6 +244,28 @@ def test_wait_pullback_without_tradeable_plan_does_not_create_order(monkeypatch)
assert list_paper_orders()["total"] == 0
def test_wait_pullback_requires_confirmed_risk_reward(monkeypatch):
monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "1")
altcoin_db.init_db()
rec_id = altcoin_db.create_recommendation(
symbol="NORR/USDT",
rec_state="蓄力",
rec_score=24,
entry_price=95,
stop_loss=90,
tp1=105,
signals=["等待回踩"],
entry_plan={"entry_action": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "rr1": 2.0},
)
rec = {"id": rec_id, "symbol": "NORR/USDT", "execution_status": "wait_pullback", "action_status": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "entry_plan": {"entry_action": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "rr1": 2.0}}
result = sync_recommendation(rec, 100, event_time="2026-05-16T10:00:00")
assert result["reason"] == "paper_order_gate_rejected"
assert "risk_reward_not_confirmed" in result["gate_reasons"]
assert list_paper_orders()["total"] == 0
def test_wait_pullback_too_far_from_entry_does_not_create_order(monkeypatch):
monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "1")
altcoin_db.init_db()
@ -267,6 +289,45 @@ def test_wait_pullback_too_far_from_entry_does_not_create_order(monkeypatch):
assert list_paper_orders()["total"] == 0
def test_buy_now_rejects_when_cumulative_leverage_exceeded(monkeypatch):
monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "1")
monkeypatch.setenv("ALPHAX_PAPER_ACCOUNT_EQUITY_USDT", "100")
monkeypatch.setenv("ALPHAX_PAPER_TRADE_NOTIONAL_USDT", "500")
monkeypatch.setenv("ALPHAX_PAPER_MAX_CUMULATIVE_LEVERAGE", "5")
monkeypatch.setenv("ALPHAX_PAPER_TRADE_FEE_RATE", "0")
monkeypatch.setenv("ALPHAX_PAPER_TRADE_SLIPPAGE_PCT", "0")
altcoin_db.init_db()
first_id = altcoin_db.create_recommendation(
symbol="LEV1/USDT",
rec_state="爆发",
rec_score=28,
entry_price=100,
stop_loss=95,
tp1=106,
signals=["当前15min即刻入场信号"],
entry_plan={"entry_action": "可即刻买入", "entry_trigger_confirmed": True, "risk_reward_ok": True},
)
second_id = altcoin_db.create_recommendation(
symbol="LEV2/USDT",
rec_state="爆发",
rec_score=28,
entry_price=100,
stop_loss=95,
tp1=106,
signals=["当前15min即刻入场信号"],
entry_plan={"entry_action": "可即刻买入", "entry_trigger_confirmed": True, "risk_reward_ok": True},
)
rows = {r["id"]: r for r in altcoin_db.get_active_recommendations_deduped(actionable_only=False)}
first = sync_recommendation(rows[first_id], 100, event_time="2026-05-16T10:00:00")
second = sync_recommendation(rows[second_id], 100, event_time="2026-05-16T10:01:00")
assert first["opened"] is True
assert second["reason"] == "cumulative_leverage_exceeded"
assert second["risk_detail"]["projected_cumulative_leverage"] > 5
assert list_paper_trades(status="open")["total"] == 1
def test_observe_only_wait_pullback_does_not_create_order(monkeypatch):
monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "1")
altcoin_db.init_db()
@ -312,7 +373,7 @@ def test_wait_pullback_paper_order_fills_when_price_touches(monkeypatch):
tp1=105,
tp2=112,
signals=["等待回踩"],
entry_plan={"entry_action": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105},
entry_plan={"entry_action": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "risk_reward_ok": True, "rr1": 2.0},
)
with altcoin_db.get_conn() as conn:
conn.execute(
@ -320,7 +381,7 @@ def test_wait_pullback_paper_order_fills_when_price_touches(monkeypatch):
(rec_id,),
)
conn.commit()
rec = {"id": rec_id, "symbol": "FILL/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}}
rec = {"id": rec_id, "symbol": "FILL/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}}
created = sync_recommendation(rec, 100, event_time="2026-05-16T10:00:00")
filled = sync_recommendation(rec, 94.9, event_time="2026-05-16T10:05:00")
@ -345,9 +406,9 @@ def test_wait_pullback_order_cancels_when_recommendation_invalid(monkeypatch):
stop_loss=90,
tp1=105,
signals=["等待回踩"],
entry_plan={"entry_action": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105},
entry_plan={"entry_action": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "risk_reward_ok": True, "rr1": 2.0},
)
rec = {"id": rec_id, "symbol": "CANCEL/USDT", "execution_status": "wait_pullback", "action_status": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "entry_plan": {"entry_action": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105}}
rec = {"id": rec_id, "symbol": "CANCEL/USDT", "execution_status": "wait_pullback", "action_status": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "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")
with altcoin_db.get_conn() as conn:
conn.execute("UPDATE recommendation SET status='invalid', execution_status='invalid' WHERE id=%s", (rec_id,))
@ -371,9 +432,9 @@ def test_wait_pullback_order_cancels_when_price_runs_too_far(monkeypatch):
stop_loss=90,
tp1=105,
signals=["等待回踩"],
entry_plan={"entry_action": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105},
entry_plan={"entry_action": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "risk_reward_ok": True, "rr1": 2.0},
)
rec = {"id": rec_id, "symbol": "FAR/USDT", "execution_status": "wait_pullback", "action_status": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "entry_plan": {"entry_action": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105}}
rec = {"id": rec_id, "symbol": "FAR/USDT", "execution_status": "wait_pullback", "action_status": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "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")
result = sync_recommendation(rec, 108, event_time="2026-05-16T10:05:00")
@ -397,9 +458,9 @@ def test_wait_pullback_order_fills_before_same_tick_stop_loss(monkeypatch):
stop_loss=90,
tp1=105,
signals=["等待回踩"],
entry_plan={"entry_action": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105},
entry_plan={"entry_action": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "risk_reward_ok": True, "rr1": 2.0},
)
rec = {"id": rec_id, "symbol": "GAP/USDT", "execution_status": "wait_pullback", "action_status": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "entry_plan": {"entry_action": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105}}
rec = {"id": rec_id, "symbol": "GAP/USDT", "execution_status": "wait_pullback", "action_status": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "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")
result = sync_recommendation(rec, 89, event_time="2026-05-16T10:05:00")
@ -431,9 +492,9 @@ def test_wait_pullback_paper_order_fill_pushes_single_combined_card(monkeypatch)
stop_loss=90,
tp1=105,
signals=["等待回踩"],
entry_plan={"entry_action": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105},
entry_plan={"entry_action": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "risk_reward_ok": True, "rr1": 2.0},
)
rec = {"id": rec_id, "symbol": "FILLPUSH/USDT", "execution_status": "wait_pullback", "action_status": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "entry_plan": {"entry_action": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105}}
rec = {"id": rec_id, "symbol": "FILLPUSH/USDT", "execution_status": "wait_pullback", "action_status": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "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")
sync_recommendation(rec, 94.9, event_time="2026-05-16T10:05:00")
@ -531,7 +592,7 @@ def test_summary_counts_pending_paper_orders(monkeypatch):
stop_loss=90,
tp1=105,
signals=["等待回踩"],
entry_plan={"entry_action": "等回踩", "entry_price": 95},
entry_plan={"entry_action": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "risk_reward_ok": True, "rr1": 2.0},
)
with altcoin_db.get_conn() as conn:
conn.execute(
@ -539,7 +600,7 @@ def test_summary_counts_pending_paper_orders(monkeypatch):
(rec_id,),
)
conn.commit()
rec = {"id": rec_id, "symbol": "COUNT/USDT", "execution_status": "wait_pullback", "action_status": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "entry_plan": {"entry_action": "等回踩", "entry_price": 95}}
rec = {"id": rec_id, "symbol": "COUNT/USDT", "execution_status": "wait_pullback", "action_status": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "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")

View File

@ -117,7 +117,7 @@ def test_price_streamer_fills_pending_paper_order():
stop_loss=90,
tp1=105,
signals=["等待回踩"],
entry_plan={"entry_action": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105},
entry_plan={"entry_action": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "risk_reward_ok": True, "rr1": 2.0},
)
with altcoin_db.get_conn() as conn:
conn.execute(
@ -125,7 +125,7 @@ def test_price_streamer_fills_pending_paper_order():
(rec_id,),
)
conn.commit()
rec = {"id": rec_id, "symbol": "PBO/USDT", "execution_status": "wait_pullback", "action_status": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "entry_plan": {"entry_action": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105}}
rec = {"id": rec_id, "symbol": "PBO/USDT", "execution_status": "wait_pullback", "action_status": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "entry_plan": {"entry_action": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "risk_reward_ok": True, "rr1": 2.0}}
created = price_streamer.handle_price_tick("PBO/USDT", 100, {"PBO/USDT": rec}, event_time="2026-05-16T10:00:00")
targets = price_streamer.load_stream_targets()
@ -138,6 +138,61 @@ def test_price_streamer_fills_pending_paper_order():
assert list_paper_trades()["items"][0]["entry_price"] == pytest.approx(95)
def test_price_streamer_prioritizes_pending_order_over_same_symbol_recommendation():
set_config("system", "paper_trading", {
"enabled": True,
"trade_notional_usdt": 5000,
"trade_leverage": 5,
"fee_rate": 0,
"slippage_pct": 0,
})
set_config("system", "price_streamer", {
"enabled": True,
"update_latest_price_cache": True,
"sync_paper_trading": True,
"include_actionable_recommendations": True,
"include_open_paper_trades": True,
})
altcoin_db.init_db()
pending_id = altcoin_db.create_recommendation(
symbol="DUP/USDT",
rec_state="蓄力",
rec_score=24,
entry_price=95,
stop_loss=90,
tp1=105,
signals=["等待回踩"],
entry_plan={"entry_action": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "risk_reward_ok": True, "rr1": 2.0},
)
active_id = altcoin_db.create_recommendation(
symbol="DUP/USDT",
rec_state="爆发",
rec_score=28,
entry_price=100,
stop_loss=95,
tp1=106,
signals=["当前15min即刻入场信号"],
entry_plan={"entry_action": "可即刻买入", "entry_trigger_confirmed": True, "risk_reward_ok": True},
)
with altcoin_db.get_conn() as conn:
conn.execute(
"UPDATE recommendation SET execution_status='wait_pullback', action_status='等回踩', display_bucket='watch_pool' WHERE id=%s",
(pending_id,),
)
conn.commit()
pending_rec = {"id": pending_id, "symbol": "DUP/USDT", "execution_status": "wait_pullback", "action_status": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "entry_plan": {"entry_action": "等回踩", "entry_price": 95, "stop_loss": 90, "tp1": 105, "risk_reward_ok": True, "rr1": 2.0}}
price_streamer.handle_price_tick("DUP/USDT", 100, {"DUP/USDT": pending_rec}, event_time="2026-05-16T10:00:00")
targets = price_streamer.load_stream_targets()
filled = price_streamer.handle_price_tick("DUP/USDT", 94.9, targets, event_time="2026-05-16T10:05:00")
assert active_id > 0
assert targets["DUP/USDT"]["id"] == pending_id
assert filled["paper_trading"]["opened"] is True
assert list_paper_orders(status="filled")["total"] == 1
assert list_paper_trades()["items"][0]["recommendation_id"] == pending_id
def test_price_streamer_builds_binance_combined_stream_url():
url = price_streamer._stream_url(["BTC/USDT", "ETH/USDT"], {"stream_url": "wss://example.test/stream"})

View File

@ -51,6 +51,23 @@ def test_record_system_error_sends_feishu_alert(monkeypatch):
assert pushed[0]["message"] == "alert me"
def test_warning_system_error_does_not_send_feishu_alert(monkeypatch):
pushed = []
monkeypatch.setattr("app.db.system_logs.push_system_error_alert", lambda item: pushed.append(item) or (True, {"StatusCode": 0}))
log_id = record_system_error(
source="price_streamer",
level="warning",
error_type="ConnectionClosedError",
message="transient websocket disconnect",
status_code=0,
)
assert log_id > 0
assert pushed == []
assert get_system_error(log_id)["level"] == "warning"
def test_admin_system_error_api_uses_local_admin():
log_id = record_system_error(
source="scheduler",