From 10e262231fc7b02a314e9a1c800982c3c309a4d9 Mon Sep 17 00:00:00 2001 From: Thigazhezhilan J Date: Wed, 3 Jun 2026 22:11:29 +0530 Subject: [PATCH] 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 --- backend/app/routers/strategy.py | 8 +- backend/app/services/paper_broker_service.py | 16 ++-- backend/app/services/run_service.py | 89 +++++++++++++------- backend/app/services/strategy_service.py | 19 +++-- backend/paper_mtm.py | 2 +- 5 files changed, 80 insertions(+), 54 deletions(-) diff --git a/backend/app/routers/strategy.py b/backend/app/routers/strategy.py index b3d1e5d..1e13e30 100644 --- a/backend/app/routers/strategy.py +++ b/backend/app/routers/strategy.py @@ -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) diff --git a/backend/app/services/paper_broker_service.py b/backend/app/services/paper_broker_service.py index db2101b..d2a1f08 100644 --- a/backend/app/services/paper_broker_service.py +++ b/backend/app/services/paper_broker_service.py @@ -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): diff --git a/backend/app/services/run_service.py b/backend/app/services/run_service.py index d507dd5..a9fb8ed 100644 --- a/backend/app/services/run_service.py +++ b/backend/app/services/run_service.py @@ -66,31 +66,48 @@ 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): - cur.execute( - """ - SELECT run_id - FROM strategy_run - WHERE user_id = %s AND status = 'RUNNING' - ORDER BY created_at DESC - LIMIT 1 - """, - (user_id,), - ) + if mode: + cur.execute( + """ + 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 + """, + (user_id,), + ) row = cur.fetchone() if row: return row[0] - cur.execute( - """ - SELECT run_id - FROM strategy_run - WHERE user_id = %s - ORDER BY created_at DESC - LIMIT 1 - """, - (user_id,), - ) + # Fallback: most recent run of same mode (or any mode if mode not specified) + if mode: + cur.execute( + """ + 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 + """, + (user_id,), + ) row = cur.fetchone() if row: return row[0] @@ -102,18 +119,26 @@ 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): - cur.execute( - """ - SELECT run_id - FROM strategy_run - WHERE user_id = %s AND status = 'RUNNING' - ORDER BY created_at DESC - LIMIT 1 - """, - (user_id,), - ) + if mode: + cur.execute( + """ + 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 + """, + (user_id,), + ) row = cur.fetchone() return row[0] if row else None diff --git a/backend/app/services/strategy_service.py b/backend/app/services/strategy_service.py index 6088918..fab9bc5 100644 --- a/backend/app/services/strategy_service.py +++ b/backend/app/services/strategy_service.py @@ -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) diff --git a/backend/paper_mtm.py b/backend/paper_mtm.py index cc0f765..6cb726c 100644 --- a/backend/paper_mtm.py +++ b/backend/paper_mtm.py @@ -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)