1
This commit is contained in:
parent
346c35d664
commit
ea342161fc
13
.env.example
13
.env.example
@ -66,9 +66,18 @@ ALPHAX_ONCHAIN_WHALE_TX_USD=250000
|
|||||||
# 策略交易挂单门控。wait_pullback 只是候选,必须通过这些条件才会创建挂单。
|
# 策略交易挂单门控。wait_pullback 只是候选,必须通过这些条件才会创建挂单。
|
||||||
ALPHAX_PAPER_ORDER_GATE_ENABLED=1
|
ALPHAX_PAPER_ORDER_GATE_ENABLED=1
|
||||||
ALPHAX_PAPER_MAX_CUMULATIVE_LEVERAGE=5
|
ALPHAX_PAPER_MAX_CUMULATIVE_LEVERAGE=5
|
||||||
ALPHAX_PAPER_ORDER_MIN_REC_SCORE=20
|
ALPHAX_PAPER_ENTRY_GATE_ENABLED=1
|
||||||
ALPHAX_PAPER_ORDER_MIN_RR=1.2
|
ALPHAX_PAPER_ENTRY_MIN_REC_SCORE=50
|
||||||
|
ALPHAX_PAPER_ENTRY_MIN_RR=1.8
|
||||||
|
ALPHAX_PAPER_MAX_STOP_LOSS_LEVERAGE_RISK_PCT=20
|
||||||
|
ALPHAX_PAPER_MAX_ACCOUNT_DRAWDOWN_PAUSE_PCT=3
|
||||||
|
ALPHAX_PAPER_PAUSE_AFTER_WEAK_ENTRIES=3
|
||||||
|
ALPHAX_PAPER_WEAK_ENTRY_WINDOW_HOURS=6
|
||||||
|
ALPHAX_PAPER_WEAK_ENTRY_MIN_MAX_PNL_PCT=1
|
||||||
|
ALPHAX_PAPER_ORDER_MIN_REC_SCORE=50
|
||||||
|
ALPHAX_PAPER_ORDER_MIN_RR=1.8
|
||||||
ALPHAX_PAPER_ORDER_REQUIRE_RISK_REWARD_OK=1
|
ALPHAX_PAPER_ORDER_REQUIRE_RISK_REWARD_OK=1
|
||||||
|
ALPHAX_PAPER_ORDER_MIN_DISTANCE_TO_ENTRY_PCT=1.5
|
||||||
ALPHAX_PAPER_ORDER_MAX_DISTANCE_TO_ENTRY_PCT=8
|
ALPHAX_PAPER_ORDER_MAX_DISTANCE_TO_ENTRY_PCT=8
|
||||||
ALPHAX_PAPER_ORDER_REQUIRE_CURRENT_TRIGGER=0
|
ALPHAX_PAPER_ORDER_REQUIRE_CURRENT_TRIGGER=0
|
||||||
ALPHAX_PAPER_ORDER_CANCEL_FAR_FROM_ENTRY_PCT=12
|
ALPHAX_PAPER_ORDER_CANCEL_FAR_FROM_ENTRY_PCT=12
|
||||||
|
|||||||
@ -118,9 +118,18 @@ 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_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),
|
"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_gate_enabled": _env_bool("ALPHAX_PAPER_ORDER_GATE_ENABLED", True),
|
||||||
"order_min_rec_score": _env_float("ALPHAX_PAPER_ORDER_MIN_REC_SCORE", 20.0),
|
"entry_gate_enabled": _env_bool("ALPHAX_PAPER_ENTRY_GATE_ENABLED", True),
|
||||||
"order_min_rr": _env_float("ALPHAX_PAPER_ORDER_MIN_RR", 1.2),
|
"entry_min_rec_score": _env_float("ALPHAX_PAPER_ENTRY_MIN_REC_SCORE", 50.0),
|
||||||
|
"entry_min_rr": _env_float("ALPHAX_PAPER_ENTRY_MIN_RR", 1.8),
|
||||||
|
"max_stop_loss_leverage_risk_pct": _env_float("ALPHAX_PAPER_MAX_STOP_LOSS_LEVERAGE_RISK_PCT", 20.0),
|
||||||
|
"max_account_drawdown_pause_pct": _env_float("ALPHAX_PAPER_MAX_ACCOUNT_DRAWDOWN_PAUSE_PCT", 3.0),
|
||||||
|
"pause_after_weak_entries": _env_int("ALPHAX_PAPER_PAUSE_AFTER_WEAK_ENTRIES", 3),
|
||||||
|
"weak_entry_window_hours": _env_float("ALPHAX_PAPER_WEAK_ENTRY_WINDOW_HOURS", 6.0),
|
||||||
|
"weak_entry_min_max_pnl_pct": _env_float("ALPHAX_PAPER_WEAK_ENTRY_MIN_MAX_PNL_PCT", 1.0),
|
||||||
|
"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),
|
"order_require_risk_reward_ok": _env_bool("ALPHAX_PAPER_ORDER_REQUIRE_RISK_REWARD_OK", True),
|
||||||
|
"order_min_distance_to_entry_pct": _env_float("ALPHAX_PAPER_ORDER_MIN_DISTANCE_TO_ENTRY_PCT", 1.5),
|
||||||
"order_max_distance_to_entry_pct": _env_float("ALPHAX_PAPER_ORDER_MAX_DISTANCE_TO_ENTRY_PCT", 8.0),
|
"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_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),
|
"order_cancel_far_from_entry_pct": _env_float("ALPHAX_PAPER_ORDER_CANCEL_FAR_FROM_ENTRY_PCT", 12.0),
|
||||||
|
|||||||
@ -102,6 +102,70 @@ def _cumulative_leverage_check(conn, additional_notional: float, config: dict |
|
|||||||
return projected_leverage <= cap + 1e-12, detail
|
return projected_leverage <= cap + 1e-12, detail
|
||||||
|
|
||||||
|
|
||||||
|
def _portfolio_drawdown_check(conn, additional_notional: float, config: dict | None = None) -> tuple[bool, dict]:
|
||||||
|
cfg = _paper_cfg(config)
|
||||||
|
cap = max(0.0, _safe_float(cfg.get("max_account_drawdown_pause_pct"), 0))
|
||||||
|
if cap <= 0:
|
||||||
|
return True, {"disabled": True, "max_account_drawdown_pause_pct": cap}
|
||||||
|
equity = default_account_equity_usdt(cfg)
|
||||||
|
open_rows = conn.execute("SELECT notional_usdt, pnl_pct FROM paper_trades WHERE status='open'").fetchall()
|
||||||
|
unrealized = sum(_safe_float(r["notional_usdt"]) * _safe_float(r["pnl_pct"]) / 100 for r in open_rows)
|
||||||
|
drawdown_pct = abs(min(0.0, unrealized)) / equity * 100 if equity > 0 else 0
|
||||||
|
detail = {
|
||||||
|
"account_equity_usdt": equity,
|
||||||
|
"open_unrealized_pnl_usdt": round(unrealized, 8),
|
||||||
|
"open_drawdown_pct": round(drawdown_pct, 6),
|
||||||
|
"max_account_drawdown_pause_pct": cap,
|
||||||
|
"additional_notional_usdt": round(max(0.0, _safe_float(additional_notional)), 8),
|
||||||
|
}
|
||||||
|
return drawdown_pct <= cap + 1e-12, detail
|
||||||
|
|
||||||
|
|
||||||
|
def _weak_entries_check(conn, event_time: str, config: dict | None = None) -> tuple[bool, dict]:
|
||||||
|
cfg = _paper_cfg(config)
|
||||||
|
limit = max(0, _safe_int(cfg.get("pause_after_weak_entries"), 0))
|
||||||
|
if limit <= 0:
|
||||||
|
return True, {"disabled": True, "pause_after_weak_entries": limit}
|
||||||
|
window_hours = max(0.1, _safe_float(cfg.get("weak_entry_window_hours"), 6.0))
|
||||||
|
threshold = max(0.0, _safe_float(cfg.get("weak_entry_min_max_pnl_pct"), 1.0))
|
||||||
|
now = _parse_time(event_time or _now()) or datetime.now()
|
||||||
|
since = (now - timedelta(hours=window_hours)).isoformat()
|
||||||
|
rows = conn.execute(
|
||||||
|
"""
|
||||||
|
SELECT symbol, opened_at, entry_price, max_price
|
||||||
|
FROM paper_trades
|
||||||
|
WHERE opened_at >= %s
|
||||||
|
ORDER BY opened_at DESC, id DESC
|
||||||
|
LIMIT %s
|
||||||
|
""",
|
||||||
|
(since, limit),
|
||||||
|
).fetchall()
|
||||||
|
samples = []
|
||||||
|
for row in rows:
|
||||||
|
entry = _safe_float(row["entry_price"])
|
||||||
|
max_pnl = (max(_safe_float(row["max_price"]), entry) / entry - 1) * 100 if entry > 0 else 0
|
||||||
|
samples.append({"symbol": row["symbol"], "opened_at": row["opened_at"], "max_pnl_pct": round(max_pnl, 6)})
|
||||||
|
enough = len(samples) >= limit
|
||||||
|
all_weak = enough and all(x["max_pnl_pct"] < threshold for x in samples)
|
||||||
|
detail = {
|
||||||
|
"pause_after_weak_entries": limit,
|
||||||
|
"weak_entry_window_hours": window_hours,
|
||||||
|
"weak_entry_min_max_pnl_pct": threshold,
|
||||||
|
"samples": samples,
|
||||||
|
}
|
||||||
|
return not all_weak, detail
|
||||||
|
|
||||||
|
|
||||||
|
def _portfolio_entry_pause_check(conn, additional_notional: float, event_time: str, config: dict | None = None) -> tuple[bool, str, dict]:
|
||||||
|
drawdown_ok, drawdown = _portfolio_drawdown_check(conn, additional_notional, config)
|
||||||
|
if not drawdown_ok:
|
||||||
|
return False, "portfolio_drawdown_pause", {"drawdown": drawdown}
|
||||||
|
weak_ok, weak = _weak_entries_check(conn, event_time, config)
|
||||||
|
if not weak_ok:
|
||||||
|
return False, "weak_entries_pause", {"weak_entries": weak}
|
||||||
|
return True, "", {"drawdown": drawdown, "weak_entries": weak}
|
||||||
|
|
||||||
|
|
||||||
def _trailing_config() -> dict:
|
def _trailing_config() -> dict:
|
||||||
cfg = paper_trading_config()
|
cfg = paper_trading_config()
|
||||||
return {
|
return {
|
||||||
@ -262,6 +326,22 @@ def _trade_pnl_pct(entry_price: float, current_price: float) -> float:
|
|||||||
return round((current_price / entry_price - 1) * 100, 4)
|
return round((current_price / entry_price - 1) * 100, 4)
|
||||||
|
|
||||||
|
|
||||||
|
def _stop_loss_distance_pct(side: str, entry_price: float, stop_loss: float) -> float:
|
||||||
|
if entry_price <= 0 or stop_loss <= 0:
|
||||||
|
return 0.0
|
||||||
|
if str(side or "long").lower() == "short":
|
||||||
|
return max(0.0, (stop_loss / entry_price - 1) * 100)
|
||||||
|
return max(0.0, (1 - stop_loss / entry_price) * 100)
|
||||||
|
|
||||||
|
|
||||||
|
def _stop_loss_leverage_risk_pct(side: str, entry_price: float, stop_loss: float, leverage: float) -> float:
|
||||||
|
return round(_stop_loss_distance_pct(side, entry_price, stop_loss) * max(1.0, _safe_float(leverage, 1.0)), 6)
|
||||||
|
|
||||||
|
|
||||||
|
def _trade_rr(side: str, entry_price: float, stop_loss: float, tp1: float) -> float:
|
||||||
|
return _paper_order_rr(side, entry_price, stop_loss, tp1)
|
||||||
|
|
||||||
|
|
||||||
def _account_return_pct(pnl_usdt: float, account_equity: float | None = None, config: dict | None = None) -> float:
|
def _account_return_pct(pnl_usdt: float, account_equity: float | None = None, config: dict | None = None) -> float:
|
||||||
equity = max(1.0, _safe_float(account_equity, default_account_equity_usdt(config)))
|
equity = max(1.0, _safe_float(account_equity, default_account_equity_usdt(config)))
|
||||||
return round(_safe_float(pnl_usdt) / equity * 100, 4)
|
return round(_safe_float(pnl_usdt) / equity * 100, 4)
|
||||||
@ -500,6 +580,51 @@ def _open_trade(conn, rec: dict, current_price: float, event_time: str, config:
|
|||||||
plan = _entry_plan(rec)
|
plan = _entry_plan(rec)
|
||||||
entry_price = _open_price(current_price, cfg)
|
entry_price = _open_price(current_price, cfg)
|
||||||
notional = default_notional_usdt(cfg)
|
notional = default_notional_usdt(cfg)
|
||||||
|
side = str(plan.get("side") or rec.get("side") or "long").strip().lower() or "long"
|
||||||
|
leverage = default_leverage(cfg)
|
||||||
|
stop_loss = _safe_float(rec.get("stop_loss") or plan.get("stop_loss"))
|
||||||
|
tp1 = _safe_float(rec.get("tp1") or plan.get("tp1") or plan.get("take_profit_1"))
|
||||||
|
rec_score = _safe_float(rec.get("rec_score") or rec.get("score"))
|
||||||
|
if rec_score <= 0 and rec_id > 0:
|
||||||
|
row = conn.execute("SELECT rec_score FROM recommendation WHERE id=%s", (rec_id,)).fetchone()
|
||||||
|
rec_score = _safe_float(row["rec_score"] if row else 0)
|
||||||
|
if bool(cfg.get("entry_gate_enabled", True)):
|
||||||
|
rr = _safe_float(plan.get("rr1") or plan.get("rr1_live")) or _trade_rr(side, entry_price, stop_loss, tp1)
|
||||||
|
min_rr = max(0.0, _safe_float(cfg.get("entry_min_rr"), 0))
|
||||||
|
min_score = max(0.0, _safe_float(cfg.get("entry_min_rec_score"), 0))
|
||||||
|
sl_risk = _stop_loss_leverage_risk_pct(side, entry_price, stop_loss, leverage)
|
||||||
|
max_sl_risk = max(0.0, _safe_float(cfg.get("max_stop_loss_leverage_risk_pct"), 0))
|
||||||
|
entry_reasons = []
|
||||||
|
if rec_score < min_score:
|
||||||
|
entry_reasons.append("rec_score_below_min")
|
||||||
|
if rr <= 0:
|
||||||
|
entry_reasons.append("missing_rr")
|
||||||
|
elif rr < min_rr:
|
||||||
|
entry_reasons.append("rr_below_min")
|
||||||
|
if max_sl_risk > 0 and sl_risk > max_sl_risk:
|
||||||
|
entry_reasons.append("stop_loss_leverage_risk_exceeded")
|
||||||
|
if entry_reasons:
|
||||||
|
return {
|
||||||
|
"opened": False,
|
||||||
|
"skipped": True,
|
||||||
|
"reason": "entry_gate_rejected",
|
||||||
|
"gate_reasons": entry_reasons,
|
||||||
|
"gate_detail": {
|
||||||
|
"rec_score": rec_score,
|
||||||
|
"min_rec_score": min_score,
|
||||||
|
"rr1": round(rr, 4) if rr > 0 else 0,
|
||||||
|
"min_rr": min_rr,
|
||||||
|
"stop_loss_leverage_risk_pct": sl_risk,
|
||||||
|
"max_stop_loss_leverage_risk_pct": max_sl_risk,
|
||||||
|
"entry_price": entry_price,
|
||||||
|
"stop_loss": stop_loss,
|
||||||
|
"tp1": tp1,
|
||||||
|
"leverage": leverage,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
pause_ok, pause_reason, pause_detail = _portfolio_entry_pause_check(conn, notional, event_time, cfg)
|
||||||
|
if not pause_ok:
|
||||||
|
return {"opened": False, "skipped": True, "reason": pause_reason, "risk_detail": pause_detail}
|
||||||
leverage_ok, leverage_detail = _cumulative_leverage_check(conn, notional, cfg, exclude_rec_id=rec_id)
|
leverage_ok, leverage_detail = _cumulative_leverage_check(conn, notional, cfg, exclude_rec_id=rec_id)
|
||||||
if not leverage_ok:
|
if not leverage_ok:
|
||||||
return {
|
return {
|
||||||
@ -508,11 +633,8 @@ def _open_trade(conn, rec: dict, current_price: float, event_time: str, config:
|
|||||||
"reason": "cumulative_leverage_exceeded",
|
"reason": "cumulative_leverage_exceeded",
|
||||||
"risk_detail": leverage_detail,
|
"risk_detail": leverage_detail,
|
||||||
}
|
}
|
||||||
leverage = default_leverage(cfg)
|
|
||||||
margin = default_margin_usdt(cfg)
|
margin = default_margin_usdt(cfg)
|
||||||
qty = round(notional / entry_price, 12) if entry_price > 0 else 0
|
qty = round(notional / entry_price, 12) if entry_price > 0 else 0
|
||||||
stop_loss = _safe_float(rec.get("stop_loss") or plan.get("stop_loss"))
|
|
||||||
tp1 = _safe_float(rec.get("tp1") or plan.get("tp1") or plan.get("take_profit_1"))
|
|
||||||
tp2 = _safe_float(rec.get("tp2") or plan.get("tp2") or plan.get("take_profit_2"))
|
tp2 = _safe_float(rec.get("tp2") or plan.get("tp2") or plan.get("take_profit_2"))
|
||||||
fee = round(notional * default_fee_rate(cfg), 8)
|
fee = round(notional * default_fee_rate(cfg), 8)
|
||||||
now = event_time or _now()
|
now = event_time or _now()
|
||||||
@ -672,6 +794,7 @@ def _paper_order_gate(rec: dict, current_price: float, config: dict | None = Non
|
|||||||
effective_rr = rr if rr > 0 else calc_rr
|
effective_rr = rr if rr > 0 else calc_rr
|
||||||
min_rr = max(0.0, _safe_float(cfg.get("order_min_rr"), 1.2))
|
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))
|
min_rec_score = max(0.0, _safe_float(cfg.get("order_min_rec_score"), 20.0))
|
||||||
|
min_distance = max(0.0, _safe_float(cfg.get("order_min_distance_to_entry_pct"), 0.0))
|
||||||
rec_score = _safe_float(rec.get("rec_score") or rec.get("score"))
|
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:
|
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()
|
row = conn.execute("SELECT rec_score FROM recommendation WHERE id=%s", (_safe_int(rec.get("id")),)).fetchone()
|
||||||
@ -713,6 +836,8 @@ def _paper_order_gate(rec: dict, current_price: float, config: dict | None = Non
|
|||||||
reasons.append("rr_below_min")
|
reasons.append("rr_below_min")
|
||||||
if effective_rr <= 0:
|
if effective_rr <= 0:
|
||||||
reasons.append("missing_rr")
|
reasons.append("missing_rr")
|
||||||
|
if distance_pct < min_distance:
|
||||||
|
reasons.append("too_close_to_entry")
|
||||||
if distance_pct > max_distance:
|
if distance_pct > max_distance:
|
||||||
reasons.append("too_far_from_entry")
|
reasons.append("too_far_from_entry")
|
||||||
if opportunity_level in {"momentum_watch", "theme_trend"} or level_max_action == "observe":
|
if opportunity_level in {"momentum_watch", "theme_trend"} or level_max_action == "observe":
|
||||||
@ -727,6 +852,7 @@ def _paper_order_gate(rec: dict, current_price: float, config: dict | None = Non
|
|||||||
"rr1": round(effective_rr, 4) if effective_rr > 0 else 0,
|
"rr1": round(effective_rr, 4) if effective_rr > 0 else 0,
|
||||||
"calc_rr1": round(calc_rr, 4) if calc_rr > 0 else 0,
|
"calc_rr1": round(calc_rr, 4) if calc_rr > 0 else 0,
|
||||||
"distance_to_entry_pct": round(distance_pct, 4),
|
"distance_to_entry_pct": round(distance_pct, 4),
|
||||||
|
"min_distance_to_entry_pct": min_distance,
|
||||||
"max_distance_to_entry_pct": max_distance,
|
"max_distance_to_entry_pct": max_distance,
|
||||||
"min_rr": min_rr,
|
"min_rr": min_rr,
|
||||||
"rec_score": rec_score,
|
"rec_score": rec_score,
|
||||||
@ -806,6 +932,17 @@ def _order_payload_from_rec(rec: dict, current_price: float, event_time: str, co
|
|||||||
|
|
||||||
def _fill_paper_order(conn, order: dict, rec: dict, current_price: float, event_time: str, config: dict | None = None) -> dict:
|
def _fill_paper_order(conn, order: dict, rec: dict, current_price: float, event_time: str, config: dict | None = None) -> dict:
|
||||||
fill_price = _safe_float(order.get("target_price")) or current_price
|
fill_price = _safe_float(order.get("target_price")) or current_price
|
||||||
|
cfg = _paper_cfg(config)
|
||||||
|
side = str(order.get("side") or "long").lower()
|
||||||
|
leverage = default_leverage(cfg)
|
||||||
|
stop_loss = _safe_float(order.get("stop_loss") or rec.get("stop_loss") or _entry_plan(rec).get("stop_loss"))
|
||||||
|
sl_risk = _stop_loss_leverage_risk_pct(side, fill_price, stop_loss, leverage)
|
||||||
|
max_sl_risk = max(0.0, _safe_float(cfg.get("max_stop_loss_leverage_risk_pct"), 0))
|
||||||
|
if max_sl_risk > 0 and sl_risk > max_sl_risk:
|
||||||
|
return _cancel_paper_order(conn, order, "stop_loss_leverage_risk_exceeded", event_time)
|
||||||
|
pause_ok, pause_reason, pause_detail = _portfolio_entry_pause_check(conn, default_notional_usdt(cfg), event_time, cfg)
|
||||||
|
if not pause_ok:
|
||||||
|
return _cancel_paper_order(conn, order, pause_reason, event_time)
|
||||||
trade_rec = dict(rec)
|
trade_rec = dict(rec)
|
||||||
plan = _entry_plan(trade_rec)
|
plan = _entry_plan(trade_rec)
|
||||||
plan.setdefault("entry_price", fill_price)
|
plan.setdefault("entry_price", fill_price)
|
||||||
@ -1485,6 +1622,104 @@ def list_paper_trade_events(limit: int = 80, offset: int = 0, symbol: str = "",
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def delete_paper_trade(trade_id: int) -> dict:
|
||||||
|
trade_id = _safe_int(trade_id)
|
||||||
|
if trade_id <= 0:
|
||||||
|
return {"deleted": False, "reason": "invalid_trade_id"}
|
||||||
|
conn = get_conn()
|
||||||
|
try:
|
||||||
|
trade = conn.execute("SELECT id, recommendation_id, symbol, status FROM paper_trades WHERE id=%s", (trade_id,)).fetchone()
|
||||||
|
if not trade:
|
||||||
|
return {"deleted": False, "reason": "not_found", "trade_id": trade_id}
|
||||||
|
event_count = conn.execute("SELECT COUNT(*) FROM paper_trade_events WHERE trade_id=%s", (trade_id,)).fetchone()[0]
|
||||||
|
conn.execute("DELETE FROM paper_trade_events WHERE trade_id=%s", (trade_id,))
|
||||||
|
conn.execute("DELETE FROM paper_trades WHERE id=%s", (trade_id,))
|
||||||
|
conn.commit()
|
||||||
|
return {
|
||||||
|
"deleted": True,
|
||||||
|
"trade_id": trade_id,
|
||||||
|
"symbol": trade["symbol"],
|
||||||
|
"status": trade["status"],
|
||||||
|
"deleted_events": int(event_count or 0),
|
||||||
|
}
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def delete_paper_order(order_id: int) -> dict:
|
||||||
|
order_id = _safe_int(order_id)
|
||||||
|
if order_id <= 0:
|
||||||
|
return {"deleted": False, "reason": "invalid_order_id"}
|
||||||
|
conn = get_conn()
|
||||||
|
try:
|
||||||
|
order = conn.execute("SELECT id, recommendation_id, symbol, status FROM paper_orders WHERE id=%s", (order_id,)).fetchone()
|
||||||
|
if not order:
|
||||||
|
return {"deleted": False, "reason": "not_found", "order_id": order_id}
|
||||||
|
conn.execute("DELETE FROM paper_orders WHERE id=%s", (order_id,))
|
||||||
|
conn.commit()
|
||||||
|
return {
|
||||||
|
"deleted": True,
|
||||||
|
"order_id": order_id,
|
||||||
|
"symbol": order["symbol"],
|
||||||
|
"status": order["status"],
|
||||||
|
}
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def reset_paper_trading_data(scope: str = "all") -> dict:
|
||||||
|
scope = str(scope or "all").strip().lower()
|
||||||
|
allowed = {"all", "trades", "orders", "events", "open_trades", "closed_trades", "completed"}
|
||||||
|
if scope not in allowed:
|
||||||
|
return {"reset": False, "reason": "invalid_scope", "scope": scope, "allowed_scopes": sorted(allowed)}
|
||||||
|
conn = get_conn()
|
||||||
|
try:
|
||||||
|
deleted = {"trades": 0, "orders": 0, "events": 0}
|
||||||
|
|
||||||
|
def delete_trades(where_sql: str = "", params: tuple = ()) -> None:
|
||||||
|
rows = conn.execute(f"SELECT id FROM paper_trades {where_sql}", params).fetchall()
|
||||||
|
ids = [int(r["id"]) for r in rows]
|
||||||
|
if not ids:
|
||||||
|
return
|
||||||
|
event_count = conn.execute("SELECT COUNT(*) FROM paper_trade_events WHERE trade_id = ANY(%s)", (ids,)).fetchone()[0]
|
||||||
|
conn.execute("DELETE FROM paper_trade_events WHERE trade_id = ANY(%s)", (ids,))
|
||||||
|
conn.execute("DELETE FROM paper_trades WHERE id = ANY(%s)", (ids,))
|
||||||
|
deleted["events"] += int(event_count or 0)
|
||||||
|
deleted["trades"] += len(ids)
|
||||||
|
|
||||||
|
def delete_orders(where_sql: str = "", params: tuple = ()) -> None:
|
||||||
|
count = conn.execute(f"SELECT COUNT(*) FROM paper_orders {where_sql}", params).fetchone()[0]
|
||||||
|
conn.execute(f"DELETE FROM paper_orders {where_sql}", params)
|
||||||
|
deleted["orders"] += int(count or 0)
|
||||||
|
|
||||||
|
if scope == "all":
|
||||||
|
deleted["events"] += int(conn.execute("SELECT COUNT(*) FROM paper_trade_events").fetchone()[0] or 0)
|
||||||
|
deleted["trades"] += int(conn.execute("SELECT COUNT(*) FROM paper_trades").fetchone()[0] or 0)
|
||||||
|
deleted["orders"] += int(conn.execute("SELECT COUNT(*) FROM paper_orders").fetchone()[0] or 0)
|
||||||
|
conn.execute("DELETE FROM paper_trade_events")
|
||||||
|
conn.execute("DELETE FROM paper_trades")
|
||||||
|
conn.execute("DELETE FROM paper_orders")
|
||||||
|
elif scope == "events":
|
||||||
|
deleted["events"] += int(conn.execute("SELECT COUNT(*) FROM paper_trade_events").fetchone()[0] or 0)
|
||||||
|
conn.execute("DELETE FROM paper_trade_events")
|
||||||
|
elif scope == "orders":
|
||||||
|
delete_orders()
|
||||||
|
elif scope == "trades":
|
||||||
|
delete_trades()
|
||||||
|
elif scope == "open_trades":
|
||||||
|
delete_trades("WHERE status='open'")
|
||||||
|
elif scope == "closed_trades":
|
||||||
|
delete_trades("WHERE status='closed'")
|
||||||
|
elif scope == "completed":
|
||||||
|
delete_trades("WHERE status='closed'")
|
||||||
|
delete_orders("WHERE status IN ('filled','expired','canceled','rejected')")
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
return {"reset": True, "scope": scope, "deleted": deleted}
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
def _report_trade_line(item: dict, closed: bool = False) -> str:
|
def _report_trade_line(item: dict, closed: bool = False) -> str:
|
||||||
symbol = item.get("symbol") or "--"
|
symbol = item.get("symbol") or "--"
|
||||||
side = "空" if str(item.get("side") or "long").lower() == "short" else "多"
|
side = "空" if str(item.get("side") or "long").lower() == "short" else "多"
|
||||||
|
|||||||
@ -15,6 +15,7 @@ from app.db.live_trading import (
|
|||||||
update_live_order_intent,
|
update_live_order_intent,
|
||||||
_row,
|
_row,
|
||||||
)
|
)
|
||||||
|
from app.config.system_config import live_trading_config
|
||||||
from app.db.schema import get_conn, init_db
|
from app.db.schema import get_conn, init_db
|
||||||
from app.integrations.binance_live import LiveTradingConfigError, build_binance_client
|
from app.integrations.binance_live import LiveTradingConfigError, build_binance_client
|
||||||
|
|
||||||
@ -206,6 +207,11 @@ def sync_paper_trade_to_live(
|
|||||||
client_factory=None,
|
client_factory=None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
init_db()
|
init_db()
|
||||||
|
cfg = live_trading_config()
|
||||||
|
if not bool(cfg.get("enabled")):
|
||||||
|
return {"ok": False, "reason": "live_trading_disabled", "items": []}
|
||||||
|
if str(cfg.get("execution_mode") or "exchange_api").strip().lower() != "exchange_api":
|
||||||
|
return {"ok": False, "reason": "live_trading_not_exchange_api", "items": []}
|
||||||
trade = _paper_trade(paper_trade_id)
|
trade = _paper_trade(paper_trade_id)
|
||||||
if not trade:
|
if not trade:
|
||||||
return {"ok": False, "reason": "paper_trade_not_found", "items": []}
|
return {"ok": False, "reason": "paper_trade_not_found", "items": []}
|
||||||
|
|||||||
@ -19,6 +19,7 @@ from app.db.paper_trading import sync_recommendation
|
|||||||
from app.db.recommendation_queries import get_active_recommendations_deduped
|
from app.db.recommendation_queries import get_active_recommendations_deduped
|
||||||
from app.db.schema import get_conn
|
from app.db.schema import get_conn
|
||||||
from app.db.system_logs import record_exception, record_system_error
|
from app.db.system_logs import record_exception, record_system_error
|
||||||
|
from app.services.live_trading_sync import sync_paper_trade_to_live
|
||||||
|
|
||||||
|
|
||||||
def _now() -> str:
|
def _now() -> str:
|
||||||
@ -149,6 +150,8 @@ def handle_price_tick(symbol: str, price: float, targets: dict[str, dict], event
|
|||||||
if not rec:
|
if not rec:
|
||||||
return {"updated_price": True, "paper_trading": {"skipped": True, "reason": "no_target"}}
|
return {"updated_price": True, "paper_trading": {"skipped": True, "reason": "no_target"}}
|
||||||
result = sync_recommendation(rec, price, event_time=event_time)
|
result = sync_recommendation(rec, price, event_time=event_time)
|
||||||
|
if result.get("trade_id") and (result.get("opened") or result.get("paper_order", {}).get("filled")):
|
||||||
|
result["live_sync"] = sync_paper_trade_to_live(int(result["trade_id"]), execute=True)
|
||||||
return {"updated_price": True, "paper_trading": result}
|
return {"updated_price": True, "paper_trading": result}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,14 @@
|
|||||||
from fastapi import APIRouter, Cookie
|
from fastapi import APIRouter, Cookie, HTTPException
|
||||||
|
|
||||||
from app.db.paper_trading import (
|
from app.db.paper_trading import (
|
||||||
|
delete_paper_order,
|
||||||
|
delete_paper_trade,
|
||||||
get_paper_trading_performance,
|
get_paper_trading_performance,
|
||||||
get_paper_trading_summary,
|
get_paper_trading_summary,
|
||||||
list_paper_orders,
|
list_paper_orders,
|
||||||
list_paper_trade_events,
|
list_paper_trade_events,
|
||||||
list_paper_trades,
|
list_paper_trades,
|
||||||
|
reset_paper_trading_data,
|
||||||
send_paper_trading_report,
|
send_paper_trading_report,
|
||||||
)
|
)
|
||||||
from app.web.shared import require_admin
|
from app.web.shared import require_admin
|
||||||
@ -64,3 +67,30 @@ async def api_paper_trading_events(
|
|||||||
async def api_paper_trading_report(days: int = 30, altcoin_session: str = Cookie(default="")):
|
async def api_paper_trading_report(days: int = 30, altcoin_session: str = Cookie(default="")):
|
||||||
require_admin(altcoin_session)
|
require_admin(altcoin_session)
|
||||||
return send_paper_trading_report(days=days)
|
return send_paper_trading_report(days=days)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/api/paper-trading/trades/{trade_id}")
|
||||||
|
async def api_delete_paper_trade(trade_id: int, altcoin_session: str = Cookie(default="")):
|
||||||
|
require_admin(altcoin_session)
|
||||||
|
result = delete_paper_trade(trade_id)
|
||||||
|
if not result.get("deleted"):
|
||||||
|
raise HTTPException(status_code=404 if result.get("reason") == "not_found" else 400, detail=result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/api/paper-trading/orders/{order_id}")
|
||||||
|
async def api_delete_paper_order(order_id: int, altcoin_session: str = Cookie(default="")):
|
||||||
|
require_admin(altcoin_session)
|
||||||
|
result = delete_paper_order(order_id)
|
||||||
|
if not result.get("deleted"):
|
||||||
|
raise HTTPException(status_code=404 if result.get("reason") == "not_found" else 400, detail=result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/paper-trading/reset")
|
||||||
|
async def api_reset_paper_trading(scope: str = "all", altcoin_session: str = Cookie(default="")):
|
||||||
|
require_admin(altcoin_session)
|
||||||
|
result = reset_paper_trading_data(scope=scope)
|
||||||
|
if not result.get("reset"):
|
||||||
|
raise HTTPException(status_code=400, detail=result)
|
||||||
|
return result
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -5,11 +5,14 @@ import pytest
|
|||||||
|
|
||||||
from app.db import altcoin_db
|
from app.db import altcoin_db
|
||||||
from app.db.paper_trading import (
|
from app.db.paper_trading import (
|
||||||
|
delete_paper_order,
|
||||||
|
delete_paper_trade,
|
||||||
get_paper_trading_performance,
|
get_paper_trading_performance,
|
||||||
get_paper_trading_summary,
|
get_paper_trading_summary,
|
||||||
list_paper_orders,
|
list_paper_orders,
|
||||||
list_paper_trade_events,
|
list_paper_trade_events,
|
||||||
list_paper_trades,
|
list_paper_trades,
|
||||||
|
reset_paper_trading_data,
|
||||||
send_paper_trading_report,
|
send_paper_trading_report,
|
||||||
sync_recommendation,
|
sync_recommendation,
|
||||||
)
|
)
|
||||||
@ -43,6 +46,17 @@ def _assert_no_paper_trading_copy(card: dict) -> None:
|
|||||||
assert "paper_trades" not in text
|
assert "paper_trades" not in text
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def legacy_paper_trade_thresholds(monkeypatch):
|
||||||
|
monkeypatch.setenv("ALPHAX_PAPER_ENTRY_GATE_ENABLED", "0")
|
||||||
|
monkeypatch.setenv("ALPHAX_PAPER_ORDER_MIN_REC_SCORE", "20")
|
||||||
|
monkeypatch.setenv("ALPHAX_PAPER_ORDER_MIN_RR", "1.2")
|
||||||
|
monkeypatch.setenv("ALPHAX_PAPER_ORDER_MIN_DISTANCE_TO_ENTRY_PCT", "0")
|
||||||
|
monkeypatch.setenv("ALPHAX_PAPER_MAX_STOP_LOSS_LEVERAGE_RISK_PCT", "0")
|
||||||
|
monkeypatch.setenv("ALPHAX_PAPER_MAX_ACCOUNT_DRAWDOWN_PAUSE_PCT", "0")
|
||||||
|
monkeypatch.setenv("ALPHAX_PAPER_PAUSE_AFTER_WEAK_ENTRIES", "0")
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def buy_now_rec(monkeypatch):
|
def buy_now_rec(monkeypatch):
|
||||||
monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "1")
|
monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "1")
|
||||||
@ -311,6 +325,118 @@ def test_wait_pullback_too_far_from_entry_does_not_create_order(monkeypatch):
|
|||||||
assert list_paper_orders()["total"] == 0
|
assert list_paper_orders()["total"] == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_wait_pullback_too_close_to_entry_does_not_create_order(monkeypatch):
|
||||||
|
monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "1")
|
||||||
|
monkeypatch.setenv("ALPHAX_PAPER_ORDER_MIN_DISTANCE_TO_ENTRY_PCT", "1.5")
|
||||||
|
altcoin_db.init_db()
|
||||||
|
rec_id = altcoin_db.create_recommendation(
|
||||||
|
symbol="CLOSEWAIT/USDT",
|
||||||
|
rec_state="蓄力",
|
||||||
|
rec_score=60,
|
||||||
|
entry_price=99,
|
||||||
|
stop_loss=95,
|
||||||
|
tp1=107,
|
||||||
|
signals=["等待回踩"],
|
||||||
|
entry_plan={"entry_action": "等回踩", "entry_price": 99, "stop_loss": 95, "tp1": 107, "risk_reward_ok": True, "rr1": 2.0},
|
||||||
|
)
|
||||||
|
rec = {"id": rec_id, "symbol": "CLOSEWAIT/USDT", "execution_status": "wait_pullback", "action_status": "等回踩", "entry_price": 99, "stop_loss": 95, "tp1": 107, "entry_plan": {"entry_action": "等回踩", "entry_price": 99, "stop_loss": 95, "tp1": 107, "risk_reward_ok": True, "rr1": 2.0}}
|
||||||
|
|
||||||
|
result = sync_recommendation(rec, 100, event_time="2026-05-16T10:00:00")
|
||||||
|
|
||||||
|
assert result["reason"] == "paper_order_gate_rejected"
|
||||||
|
assert "too_close_to_entry" in result["gate_reasons"]
|
||||||
|
assert list_paper_orders()["total"] == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_buy_now_rejects_large_leveraged_stop_loss(monkeypatch):
|
||||||
|
monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "1")
|
||||||
|
monkeypatch.setenv("ALPHAX_PAPER_ENTRY_GATE_ENABLED", "1")
|
||||||
|
monkeypatch.setenv("ALPHAX_PAPER_TRADE_LEVERAGE", "5")
|
||||||
|
monkeypatch.setenv("ALPHAX_PAPER_ENTRY_MIN_REC_SCORE", "50")
|
||||||
|
monkeypatch.setenv("ALPHAX_PAPER_ENTRY_MIN_RR", "1.8")
|
||||||
|
monkeypatch.setenv("ALPHAX_PAPER_MAX_STOP_LOSS_LEVERAGE_RISK_PCT", "20")
|
||||||
|
altcoin_db.init_db()
|
||||||
|
rec_id = altcoin_db.create_recommendation(
|
||||||
|
symbol="WIDESTOP/USDT",
|
||||||
|
rec_state="爆发",
|
||||||
|
rec_score=60,
|
||||||
|
entry_price=100,
|
||||||
|
stop_loss=90,
|
||||||
|
tp1=120,
|
||||||
|
signals=["当前15min即刻入场信号"],
|
||||||
|
entry_plan={"entry_action": "可即刻买入", "entry_price": 100, "stop_loss": 90, "tp1": 120, "risk_reward_ok": True, "rr1": 2.0, "entry_trigger_confirmed": True},
|
||||||
|
)
|
||||||
|
rec = next(r for r in altcoin_db.get_active_recommendations_deduped(actionable_only=False) if r["id"] == rec_id)
|
||||||
|
|
||||||
|
result = sync_recommendation(rec, 100, event_time="2026-05-16T10:00:00")
|
||||||
|
|
||||||
|
assert result["reason"] == "entry_gate_rejected"
|
||||||
|
assert "stop_loss_leverage_risk_exceeded" in result["gate_reasons"]
|
||||||
|
assert list_paper_trades()["total"] == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_buy_now_pauses_when_portfolio_drawdown_exceeded(monkeypatch, buy_now_rec):
|
||||||
|
monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "1")
|
||||||
|
monkeypatch.setenv("ALPHAX_PAPER_ACCOUNT_EQUITY_USDT", "1000")
|
||||||
|
monkeypatch.setenv("ALPHAX_PAPER_TRADE_NOTIONAL_USDT", "100")
|
||||||
|
monkeypatch.setenv("ALPHAX_PAPER_MAX_ACCOUNT_DRAWDOWN_PAUSE_PCT", "3")
|
||||||
|
monkeypatch.setenv("ALPHAX_PAPER_ENTRY_GATE_ENABLED", "0")
|
||||||
|
altcoin_db.init_db()
|
||||||
|
with altcoin_db.get_conn() as conn:
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO paper_trades (
|
||||||
|
recommendation_id, symbol, side, status, opened_at, entry_price,
|
||||||
|
qty, notional_usdt, stop_loss, tp1, tp2, max_price, min_price,
|
||||||
|
current_price, pnl_pct, source_status, source_action,
|
||||||
|
strategy_version, created_at, updated_at, margin_usdt, leverage
|
||||||
|
) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
9001,
|
||||||
|
"DRAWDOWN/USDT",
|
||||||
|
"long",
|
||||||
|
"open",
|
||||||
|
"2026-05-16T09:00:00",
|
||||||
|
100,
|
||||||
|
1,
|
||||||
|
100,
|
||||||
|
1,
|
||||||
|
120,
|
||||||
|
130,
|
||||||
|
100,
|
||||||
|
60,
|
||||||
|
60,
|
||||||
|
-40,
|
||||||
|
"test",
|
||||||
|
"test",
|
||||||
|
"",
|
||||||
|
"2026-05-16T09:00:00",
|
||||||
|
"2026-05-16T09:01:00",
|
||||||
|
20,
|
||||||
|
5,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
altcoin_db.init_db()
|
||||||
|
rec_id = altcoin_db.create_recommendation(
|
||||||
|
symbol="PAUSE/USDT",
|
||||||
|
rec_state="爆发",
|
||||||
|
rec_score=80,
|
||||||
|
entry_price=100,
|
||||||
|
stop_loss=95,
|
||||||
|
tp1=110,
|
||||||
|
signals=["当前15min即刻入场信号"],
|
||||||
|
entry_plan={"entry_action": "可即刻买入", "entry_trigger_confirmed": True, "risk_reward_ok": True, "rr1": 2.0},
|
||||||
|
)
|
||||||
|
rec = next(r for r in altcoin_db.get_active_recommendations_deduped(actionable_only=False) if r["id"] == rec_id)
|
||||||
|
|
||||||
|
result = sync_recommendation(rec, 100, event_time="2026-05-16T10:02:00")
|
||||||
|
|
||||||
|
assert result["reason"] == "portfolio_drawdown_pause"
|
||||||
|
assert list_paper_trades(status="open")["total"] == 1
|
||||||
|
|
||||||
|
|
||||||
def test_buy_now_rejects_when_cumulative_leverage_exceeded(monkeypatch):
|
def test_buy_now_rejects_when_cumulative_leverage_exceeded(monkeypatch):
|
||||||
monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "1")
|
monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "1")
|
||||||
monkeypatch.setenv("ALPHAX_PAPER_ACCOUNT_EQUITY_USDT", "100")
|
monkeypatch.setenv("ALPHAX_PAPER_ACCOUNT_EQUITY_USDT", "100")
|
||||||
@ -796,3 +922,67 @@ def test_disabled_paper_trading_skips_without_writing(monkeypatch, buy_now_rec):
|
|||||||
assert result["skipped"] is True
|
assert result["skipped"] is True
|
||||||
assert result["reason"] == "disabled"
|
assert result["reason"] == "disabled"
|
||||||
assert list_paper_trades()["total"] == 0
|
assert list_paper_trades()["total"] == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_paper_trade_removes_related_events(buy_now_rec):
|
||||||
|
sync_recommendation(buy_now_rec, 100, event_time="2026-05-16T10:00:00")
|
||||||
|
trade = list_paper_trades()["items"][0]
|
||||||
|
assert list_paper_trade_events()["total"] == 1
|
||||||
|
|
||||||
|
result = delete_paper_trade(trade["id"])
|
||||||
|
|
||||||
|
assert result["deleted"] is True
|
||||||
|
assert result["deleted_events"] == 1
|
||||||
|
assert list_paper_trades()["total"] == 0
|
||||||
|
assert list_paper_trade_events()["total"] == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_paper_order_removes_order_only(monkeypatch):
|
||||||
|
monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "1")
|
||||||
|
altcoin_db.init_db()
|
||||||
|
rec_id = altcoin_db.create_recommendation(
|
||||||
|
symbol="DELORD/USDT",
|
||||||
|
rec_state="蓄力",
|
||||||
|
rec_score=22,
|
||||||
|
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},
|
||||||
|
)
|
||||||
|
rec = {"id": rec_id, "symbol": "DELORD/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")
|
||||||
|
order = list_paper_orders()["items"][0]
|
||||||
|
|
||||||
|
result = delete_paper_order(order["id"])
|
||||||
|
|
||||||
|
assert result["deleted"] is True
|
||||||
|
assert list_paper_orders()["total"] == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_reset_paper_trading_data_all_clears_ledger(monkeypatch, buy_now_rec):
|
||||||
|
monkeypatch.setenv("ALPHAX_PAPER_TRADING_ENABLED", "1")
|
||||||
|
sync_recommendation(buy_now_rec, 100, event_time="2026-05-16T10:00:00")
|
||||||
|
altcoin_db.init_db()
|
||||||
|
rec_id = altcoin_db.create_recommendation(
|
||||||
|
symbol="RESETORD/USDT",
|
||||||
|
rec_state="蓄力",
|
||||||
|
rec_score=22,
|
||||||
|
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},
|
||||||
|
)
|
||||||
|
rec = {"id": rec_id, "symbol": "RESETORD/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 = reset_paper_trading_data("all")
|
||||||
|
|
||||||
|
assert result["reset"] is True
|
||||||
|
assert result["deleted"]["trades"] == 1
|
||||||
|
assert result["deleted"]["orders"] == 1
|
||||||
|
assert result["deleted"]["events"] == 1
|
||||||
|
assert list_paper_trades()["total"] == 0
|
||||||
|
assert list_paper_orders()["total"] == 0
|
||||||
|
assert list_paper_trade_events()["total"] == 0
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user