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:
Thigazhezhilan J 2026-06-03 22:11:29 +05:30
parent d4e06d9211
commit 10e262231f
5 changed files with 80 additions and 54 deletions

View File

@ -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)

View File

@ -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):

View File

@ -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

View File

@ -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)

View File

@ -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)