fix: surface SIP_NO_FILL warnings and prevent silent fund failures
- execution.py: dual-write SIP_NO_FILL and SIP_PARTIAL to engine_event so the strategy summary can surface them to users - execution.py: emit SIP_NO_FILL event (with cash/required amounts) on the paper path instead of silently returning when funds are insufficient - strategy_service.py: improve insufficient_funds message to show exact shortfall and reassure user that next SIP will auto-execute when funded - strategy_service.py: clear SIP_NO_FILL warning after a successful SIP_TRIGGERED so it does not persist after funds are added - runner.py: always write PRICE_FETCH_ERROR and HISTORY_LOAD_ERROR to engine_event regardless of ENGINE_DEBUG flag - db.py (backend + engine): raise default pool sizes to 20/50 max connections to handle 100 concurrent users without pool exhaustion Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
98ef7701d1
commit
298d245048
@ -94,8 +94,8 @@ def get_database_url(cfg: dict[str, str | int] | None = None) -> str:
|
||||
|
||||
def _create_engine() -> Engine:
|
||||
cfg = _db_config()
|
||||
pool_size = int(os.getenv("DB_POOL_SIZE", os.getenv("DB_POOL_MIN", "5")))
|
||||
max_overflow = int(os.getenv("DB_POOL_MAX", "10"))
|
||||
pool_size = int(os.getenv("DB_POOL_SIZE", os.getenv("DB_POOL_MIN", "20")))
|
||||
max_overflow = int(os.getenv("DB_POOL_MAX", "30"))
|
||||
pool_timeout = int(os.getenv("DB_POOL_TIMEOUT", "30"))
|
||||
engine = create_engine(
|
||||
get_database_url(cfg),
|
||||
|
||||
@ -1093,12 +1093,23 @@ def _issue_message(event: str, message: str | None, data: dict | None, meta: dic
|
||||
|
||||
if event == "SIP_NO_FILL":
|
||||
if reason_key == "insufficient_funds":
|
||||
return "Insufficient funds for this SIP."
|
||||
cash = payload.get("cash")
|
||||
required = payload.get("required")
|
||||
if cash is not None and required is not None:
|
||||
shortfall = float(required) - float(cash)
|
||||
return (
|
||||
f"Insufficient funds — ₹{shortfall:,.0f} short for this SIP. "
|
||||
"Add funds and the next SIP will execute automatically."
|
||||
)
|
||||
return (
|
||||
"Insufficient funds for this SIP. "
|
||||
"Add funds and the next SIP will execute automatically."
|
||||
)
|
||||
if reason_key == "broker_auth_expired":
|
||||
return "Broker session expired. Reconnect broker."
|
||||
if reason_key == "no_fill":
|
||||
return "Order was not filled."
|
||||
return f"SIP not executed: {_humanize_reason(reason) or 'Unknown reason'}."
|
||||
return "Order was not filled. The strategy will retry at the next interval."
|
||||
return f"SIP not executed: {_humanize_reason(reason) or 'Unknown reason'}. The strategy will retry automatically."
|
||||
|
||||
if event == "BROKER_AUTH_EXPIRED":
|
||||
return "Broker session expired. Reconnect broker."
|
||||
@ -1238,10 +1249,15 @@ def get_strategy_summary(user_id: str):
|
||||
'ORDER_REJECTED',
|
||||
'ORDER_CANCELLED'
|
||||
)
|
||||
AND ts > COALESCE(
|
||||
(SELECT MAX(ts) FROM engine_event
|
||||
WHERE user_id = %s AND run_id = %s AND event = 'SIP_TRIGGERED'),
|
||||
'1900-01-01'::timestamptz
|
||||
)
|
||||
ORDER BY ts DESC
|
||||
LIMIT 1
|
||||
""",
|
||||
(user_id, run_id),
|
||||
(user_id, run_id, user_id, run_id),
|
||||
)
|
||||
issue_row = cur.fetchone()
|
||||
|
||||
|
||||
@ -53,8 +53,8 @@ def _db_config():
|
||||
def _init_pool():
|
||||
config = _db_config()
|
||||
config = {k: v for k, v in config.items() if v is not None}
|
||||
minconn = int(os.getenv("DB_POOL_MIN", "1"))
|
||||
maxconn = int(os.getenv("DB_POOL_MAX", "10"))
|
||||
minconn = int(os.getenv("DB_POOL_MIN", "2"))
|
||||
maxconn = int(os.getenv("DB_POOL_MAX", "50"))
|
||||
if "dsn" in config:
|
||||
return pool.ThreadedConnectionPool(minconn, maxconn, dsn=config["dsn"])
|
||||
return pool.ThreadedConnectionPool(minconn, maxconn, **config)
|
||||
|
||||
@ -371,6 +371,9 @@ def _record_reconciliation_event(cur, event: str, *, logical_time, payload: dict
|
||||
return
|
||||
if event in {"SIP_EXECUTED", "SIP_PARTIAL", "SIP_NO_FILL", "ORDER_RECONCILIATION_PENDING"}:
|
||||
log_event(event, payload, cur=cur, ts=ts, logical_time=logical_time)
|
||||
# Dual-write warning events to engine_event so the strategy summary can surface them
|
||||
if event in {"SIP_NO_FILL", "SIP_PARTIAL"}:
|
||||
insert_engine_event(cur, event, data=payload, ts=ts)
|
||||
return
|
||||
insert_engine_event(cur, event, data=payload, ts=ts)
|
||||
|
||||
@ -680,6 +683,14 @@ def _try_execute_sip_paper(
|
||||
funds = broker.get_funds(cur=cur)
|
||||
cash = funds.get("cash")
|
||||
if cash is not None and float(cash) < sip_amount_val:
|
||||
if not event_exists("SIP_NO_FILL", logical_time, cur=cur):
|
||||
_payload = {
|
||||
"reason": "insufficient_funds",
|
||||
"cash": float(cash),
|
||||
"required": sip_amount_val,
|
||||
}
|
||||
log_event("SIP_NO_FILL", _payload, cur=cur, ts=event_ts, logical_time=logical_time)
|
||||
insert_engine_event(cur, "SIP_NO_FILL", data=_payload, ts=event_ts)
|
||||
return state, False
|
||||
|
||||
log_event(
|
||||
|
||||
@ -533,6 +533,7 @@ def _engine_loop(config, stop_event: threading.Event):
|
||||
_pause_for_auth_expiry(scope_user, scope_run, str(exc), emit_event_cb=emit_event_cb)
|
||||
break
|
||||
except Exception as exc:
|
||||
log_event("PRICE_FETCH_ERROR", {"error": str(exc)})
|
||||
debug_event("PRICE_FETCH_ERROR", "live price fetch failed", {"error": str(exc)})
|
||||
if not sleep_with_heartbeat(30, stop_event, scope_user, scope_run, owner_id):
|
||||
exit_reason = "LEASE_LOST"
|
||||
@ -556,6 +557,7 @@ def _engine_loop(config, stop_event: threading.Event):
|
||||
_pause_for_auth_expiry(scope_user, scope_run, str(exc), emit_event_cb=emit_event_cb)
|
||||
break
|
||||
except Exception as exc:
|
||||
log_event("HISTORY_LOAD_ERROR", {"error": str(exc)})
|
||||
debug_event("HISTORY_LOAD_ERROR", "history load failed", {"error": str(exc)})
|
||||
if not sleep_with_heartbeat(30, stop_event, scope_user, scope_run, owner_id):
|
||||
exit_reason = "LEASE_LOST"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user