Add resume strategy functionality and update run status handling

This commit is contained in:
Thigazhezhilan J 2026-03-28 13:06:35 +05:30
parent 5f29d8c757
commit 79c036ca8b
3 changed files with 105 additions and 14 deletions

View File

@ -2,6 +2,7 @@ from fastapi import APIRouter, Query, Request
from app.models import StrategyStartRequest
from app.services.strategy_service import (
start_strategy,
resume_strategy,
stop_strategy,
get_strategy_status,
get_strategy_summary,
@ -23,6 +24,11 @@ def stop(request: Request):
user_id = get_request_user_id(request)
return stop_strategy(user_id)
@router.post("/strategy/resume")
def resume(request: Request):
user_id = get_request_user_id(request)
return resume_strategy(user_id)
@router.get("/strategy/status")
def status(request: Request):
user_id = get_request_user_id(request)

View File

@ -157,7 +157,8 @@ def update_run_status(user_id: str, run_id: str, status: str, meta: dict | None
cur.execute(
"""
UPDATE strategy_run
SET status = %s, started_at = COALESCE(started_at, %s), meta = COALESCE(meta, '{}'::jsonb) || %s
SET status = %s, started_at = COALESCE(started_at, %s), stopped_at = NULL,
meta = COALESCE(meta, '{}'::jsonb) || %s
WHERE run_id = %s AND user_id = %s
""",
(status, now, Json(meta or {}), run_id, user_id),

View File

@ -69,6 +69,16 @@ def stop_run(user_id: str, run_id: str, reason="user_request"):
)
def resume_run(user_id: str, run_id: str):
emit_event(
user_id=user_id,
run_id=run_id,
event="STRATEGY_RESUMED",
message="Strategy resumed",
meta={},
)
def emit_event(
*,
user_id: str,
@ -270,6 +280,15 @@ def deactivate_strategy_config(user_id: str, run_id: str):
cfg["active"] = False
_save_config(cfg, user_id, run_id)
def reactivate_strategy_config(user_id: str, run_id: str):
cfg = _load_config(user_id, run_id)
if not cfg:
return {}
cfg["active"] = True
_save_config(cfg, user_id, run_id)
return cfg
def _write_status(user_id: str, run_id: str, status):
now_local = datetime.now().astimezone()
with db_connection() as conn:
@ -569,8 +588,61 @@ def stop_strategy(user_id: str):
return {"status": "stopped"}
def resume_strategy(user_id: str):
engine_external = os.getenv("ENGINE_EXTERNAL", "").strip().lower() in {"1", "true", "yes"}
running_run_id = get_running_run_id(user_id)
if running_run_id:
return {"status": "already_running", "run_id": running_run_id}
run_id = get_active_run_id(user_id)
cfg = _load_config(user_id, run_id)
strategy_name = (cfg.get("strategy") or "").strip()
mode = (cfg.get("mode") or "").strip().upper()
if not strategy_name or mode not in {"LIVE", "PAPER"}:
return {"status": "no_resumable_run"}
if mode == "LIVE":
is_valid, broker_state, _failure_reason = _validate_live_broker_session(user_id)
if not is_valid:
return {
"status": "broker_auth_required",
"redirect_url": "/api/broker/login",
"broker": broker_state.get("broker"),
}
reactivate_strategy_config(user_id, run_id)
resume_run(user_id, run_id)
_write_status(user_id, run_id, "RUNNING")
update_run_status(user_id, run_id, "RUNNING", meta={"reason": "user_resume"})
if not engine_external:
engine_config = _build_engine_config(user_id, run_id, None)
if not engine_config:
return {"status": "resume_failed", "run_id": run_id}
started = start_engine(engine_config)
if not started:
return {"status": "resume_failed", "run_id": run_id}
_write_status(user_id, run_id, "RUNNING")
try:
user = get_user_by_id(user_id)
if user:
body = (
"Your strategy has been resumed.\n\n"
f"Strategy: {strategy_name}\n"
f"Mode: {mode}\n"
f"Run ID: {run_id}\n"
)
send_email_async(user["username"], "Strategy resumed", body)
except Exception:
pass
return {"status": "resumed", "run_id": run_id}
def get_strategy_status(user_id: str):
run_id = get_active_run_id(user_id)
cfg = _load_config(user_id, run_id) if run_id else {}
with db_connection() as conn:
with conn.cursor() as cur:
cur.execute(
@ -585,12 +657,7 @@ def get_strategy_status(user_id: str):
"status": row[0],
"last_updated": _format_local_ts(row[1]),
}
if status.get("status") == "RUNNING":
cfg = _load_config(user_id, run_id)
mode = (cfg.get("mode") or "LIVE").strip().upper()
with engine_context(user_id, run_id):
state = load_state(mode=mode)
last_execution_ts = _last_execution_ts(state, mode)
status["run_id"] = run_id
sip_frequency = cfg.get("sip_frequency")
if not isinstance(sip_frequency, dict):
frequency = cfg.get("frequency")
@ -603,6 +670,19 @@ def get_strategy_status(user_id: str):
unit = unit or "days"
if frequency is not None and unit:
sip_frequency = {"value": frequency, "unit": unit}
status["config"] = {
"strategy": cfg.get("strategy"),
"sip_amount": cfg.get("sip_amount"),
"sip_frequency": sip_frequency,
"mode": cfg.get("mode"),
"broker": cfg.get("broker"),
"active": cfg.get("active"),
}
if status.get("status") == "RUNNING":
mode = (cfg.get("mode") or "LIVE").strip().upper()
with engine_context(user_id, run_id):
state = load_state(mode=mode)
last_execution_ts = _last_execution_ts(state, mode)
next_eligible = compute_next_eligible(last_execution_ts, sip_frequency)
status["last_execution_ts"] = last_execution_ts
status["next_eligible_ts"] = next_eligible
@ -614,6 +694,10 @@ def get_strategy_status(user_id: str):
status["status"] = "WAITING"
except ValueError:
pass
status_key = (status.get("status") or "IDLE").upper()
resumable = bool(cfg.get("strategy")) and bool(cfg.get("mode"))
status["can_resume"] = resumable and status_key in {"STOPPED", "PAUSED_AUTH_EXPIRED"}
status["can_restart"] = resumable and status_key in {"STOPPED", "PAUSED_AUTH_EXPIRED"}
return status
def get_engine_status(user_id: str):