feat: make paper and live trading fully independent
- start_strategy filters running check by mode so starting LIVE won't clash with an active PAPER run and vice versa - stop_strategy and resume_strategy accept optional mode param so each tab stops/resumes only its own run - paper_broker_service scopes all run lookups to mode=PAPER - paper_mtm scopes run lookup to mode=PAPER - routers/strategy exposes ?mode= query param on /stop and /resume - run_service get_active_run_id and get_running_run_id already support mode filtering (added in previous session) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d4e06d9211
commit
10e262231f
@ -34,10 +34,10 @@ def start(req: StrategyStartRequest, request: Request):
|
||||
return start_strategy(req, user_id)
|
||||
|
||||
@router.post("/strategy/stop")
|
||||
def stop(request: Request):
|
||||
def stop(request: Request, mode: str | None = Query(None)):
|
||||
try:
|
||||
user_id = get_request_user_id(request)
|
||||
result = stop_strategy(user_id)
|
||||
result = stop_strategy(user_id, mode=mode)
|
||||
if result.get("status") not in {"stopped", "already_stopped"}:
|
||||
_raise_strategy_error(result, default_status=http_status.HTTP_409_CONFLICT)
|
||||
return result
|
||||
@ -51,10 +51,10 @@ def stop(request: Request):
|
||||
) from exc
|
||||
|
||||
@router.post("/strategy/resume")
|
||||
def resume(request: Request):
|
||||
def resume(request: Request, mode: str | None = Query(None)):
|
||||
try:
|
||||
user_id = get_request_user_id(request)
|
||||
result = resume_strategy(user_id)
|
||||
result = resume_strategy(user_id, mode=mode)
|
||||
success_statuses = {"resumed", "already_running"}
|
||||
if result.get("status") == "broker_auth_required":
|
||||
_raise_strategy_error(result, default_status=http_status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
@ -34,19 +34,19 @@ def _broker():
|
||||
|
||||
|
||||
def get_paper_broker(user_id: str):
|
||||
run_id = get_active_run_id(user_id)
|
||||
run_id = get_active_run_id(user_id, mode="PAPER")
|
||||
with engine_context(user_id, run_id):
|
||||
return _broker()
|
||||
|
||||
|
||||
def get_funds(user_id: str):
|
||||
run_id = get_active_run_id(user_id)
|
||||
run_id = get_active_run_id(user_id, mode="PAPER")
|
||||
with engine_context(user_id, run_id):
|
||||
return _broker().get_funds()
|
||||
|
||||
|
||||
def get_positions(user_id: str):
|
||||
run_id = get_active_run_id(user_id)
|
||||
run_id = get_active_run_id(user_id, mode="PAPER")
|
||||
with engine_context(user_id, run_id):
|
||||
positions = _broker().get_positions()
|
||||
enriched = []
|
||||
@ -67,19 +67,19 @@ def get_positions(user_id: str):
|
||||
|
||||
|
||||
def get_orders(user_id: str):
|
||||
run_id = get_active_run_id(user_id)
|
||||
run_id = get_active_run_id(user_id, mode="PAPER")
|
||||
with engine_context(user_id, run_id):
|
||||
return _broker().get_orders()
|
||||
|
||||
|
||||
def get_trades(user_id: str):
|
||||
run_id = get_active_run_id(user_id)
|
||||
run_id = get_active_run_id(user_id, mode="PAPER")
|
||||
with engine_context(user_id, run_id):
|
||||
return _broker().get_trades()
|
||||
|
||||
|
||||
def get_equity_curve(user_id: str):
|
||||
run_id = get_active_run_id(user_id)
|
||||
run_id = get_active_run_id(user_id, mode="PAPER")
|
||||
with engine_context(user_id, run_id):
|
||||
broker = _broker()
|
||||
points = broker.get_equity_curve()
|
||||
@ -107,7 +107,7 @@ def get_equity_curve(user_id: str):
|
||||
def add_cash(user_id: str, amount: float):
|
||||
if amount <= 0:
|
||||
raise ValueError("Amount must be positive")
|
||||
run_id = get_running_run_id(user_id)
|
||||
run_id = get_running_run_id(user_id, mode="PAPER")
|
||||
if not run_id:
|
||||
raise ValueError("Strategy must be running to add cash")
|
||||
|
||||
@ -142,7 +142,7 @@ def add_cash(user_id: str, amount: float):
|
||||
|
||||
|
||||
def reset_paper_state(user_id: str):
|
||||
run_id = get_active_run_id(user_id)
|
||||
run_id = get_active_run_id(user_id, mode="PAPER")
|
||||
|
||||
def _op(cur, _conn):
|
||||
with engine_context(user_id, run_id):
|
||||
|
||||
@ -66,28 +66,45 @@ def ensure_default_run(user_id: str):
|
||||
return run_with_retry(_op)
|
||||
|
||||
|
||||
def get_active_run_id(user_id: str):
|
||||
def get_active_run_id(user_id: str, mode: str | None = None):
|
||||
def _op(cur, _conn):
|
||||
if mode:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT run_id
|
||||
FROM strategy_run
|
||||
SELECT run_id FROM strategy_run
|
||||
WHERE user_id = %s AND status = 'RUNNING' AND UPPER(mode) = %s
|
||||
ORDER BY created_at DESC LIMIT 1
|
||||
""",
|
||||
(user_id, mode.strip().upper()),
|
||||
)
|
||||
else:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT run_id FROM strategy_run
|
||||
WHERE user_id = %s AND status = 'RUNNING'
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
ORDER BY created_at DESC LIMIT 1
|
||||
""",
|
||||
(user_id,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if row:
|
||||
return row[0]
|
||||
# Fallback: most recent run of same mode (or any mode if mode not specified)
|
||||
if mode:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT run_id
|
||||
FROM strategy_run
|
||||
SELECT run_id FROM strategy_run
|
||||
WHERE user_id = %s AND UPPER(mode) = %s
|
||||
ORDER BY created_at DESC LIMIT 1
|
||||
""",
|
||||
(user_id, mode.strip().upper()),
|
||||
)
|
||||
else:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT run_id FROM strategy_run
|
||||
WHERE user_id = %s
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
ORDER BY created_at DESC LIMIT 1
|
||||
""",
|
||||
(user_id,),
|
||||
)
|
||||
@ -102,15 +119,23 @@ def get_active_run_id(user_id: str):
|
||||
return ensure_default_run(user_id)
|
||||
|
||||
|
||||
def get_running_run_id(user_id: str):
|
||||
def get_running_run_id(user_id: str, mode: str | None = None):
|
||||
def _op(cur, _conn):
|
||||
if mode:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT run_id
|
||||
FROM strategy_run
|
||||
SELECT run_id FROM strategy_run
|
||||
WHERE user_id = %s AND status = 'RUNNING' AND UPPER(mode) = %s
|
||||
ORDER BY created_at DESC LIMIT 1
|
||||
""",
|
||||
(user_id, mode.strip().upper()),
|
||||
)
|
||||
else:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT run_id FROM strategy_run
|
||||
WHERE user_id = %s AND status = 'RUNNING'
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
ORDER BY created_at DESC LIMIT 1
|
||||
""",
|
||||
(user_id,),
|
||||
)
|
||||
|
||||
@ -328,8 +328,8 @@ def _get_engine_status_row(user_id: str, run_id: str):
|
||||
return {"status": row[0], "last_updated": row[1]}
|
||||
|
||||
|
||||
def _effective_running_run_id(user_id: str):
|
||||
run_id = get_running_run_id(user_id)
|
||||
def _effective_running_run_id(user_id: str, mode: str | None = None):
|
||||
run_id = get_running_run_id(user_id, mode=mode)
|
||||
if not run_id:
|
||||
return None
|
||||
engine_row = _get_engine_status_row(user_id, run_id)
|
||||
@ -557,7 +557,8 @@ def _last_execution_ts(state: dict, mode: str) -> str | None:
|
||||
|
||||
def start_strategy(req, user_id: str):
|
||||
engine_external = os.getenv("ENGINE_EXTERNAL", "").strip().lower() in {"1", "true", "yes"}
|
||||
running_run_id = _effective_running_run_id(user_id)
|
||||
req_mode = (req.mode or "PAPER").strip().upper()
|
||||
running_run_id = _effective_running_run_id(user_id, mode=req_mode)
|
||||
if running_run_id:
|
||||
running_cfg = _load_config(user_id, running_run_id)
|
||||
running_mode = (running_cfg.get("mode") or req.mode or "PAPER").strip().upper()
|
||||
@ -775,10 +776,10 @@ def resume_running_runs():
|
||||
if started:
|
||||
_write_status(user_id, run_id, "RUNNING")
|
||||
|
||||
def stop_strategy(user_id: str):
|
||||
run_id = _effective_running_run_id(user_id)
|
||||
def stop_strategy(user_id: str, mode: str | None = None):
|
||||
run_id = _effective_running_run_id(user_id, mode=mode)
|
||||
if not run_id:
|
||||
latest_run_id = get_running_run_id(user_id) or get_active_run_id(user_id)
|
||||
latest_run_id = get_running_run_id(user_id, mode=mode) or get_active_run_id(user_id, mode=mode)
|
||||
return {"status": "already_stopped", "run_id": latest_run_id}
|
||||
|
||||
engine_external = os.getenv("ENGINE_EXTERNAL", "").strip().lower() in {"1", "true", "yes"}
|
||||
@ -820,13 +821,13 @@ def stop_strategy(user_id: str):
|
||||
return result
|
||||
|
||||
|
||||
def resume_strategy(user_id: str):
|
||||
def resume_strategy(user_id: str, mode: str | None = None):
|
||||
engine_external = os.getenv("ENGINE_EXTERNAL", "").strip().lower() in {"1", "true", "yes"}
|
||||
running_run_id = _effective_running_run_id(user_id)
|
||||
running_run_id = _effective_running_run_id(user_id, mode=mode)
|
||||
if running_run_id:
|
||||
return {"status": "already_running", "run_id": running_run_id}
|
||||
|
||||
run_id = get_active_run_id(user_id)
|
||||
run_id = get_active_run_id(user_id, mode=mode)
|
||||
if not run_id:
|
||||
return {"status": "no_resumable_run"}
|
||||
cfg = _load_config(user_id, run_id)
|
||||
|
||||
@ -23,7 +23,7 @@ router = APIRouter(prefix="/api/paper", tags=["paper-mtm"])
|
||||
@router.get("/mtm")
|
||||
def paper_mtm(request: Request) -> Dict[str, Any]:
|
||||
user_id = get_request_user_id(request)
|
||||
run_id = get_active_run_id(user_id)
|
||||
run_id = get_active_run_id(user_id, mode="PAPER")
|
||||
with engine_context(user_id, run_id):
|
||||
broker = get_paper_broker(user_id)
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user