# engine/execution.py from datetime import datetime, timezone from indian_paper_trading_strategy.engine.state import load_state, save_state from indian_paper_trading_strategy.engine.broker import Broker from indian_paper_trading_strategy.engine.ledger import log_event, event_exists from indian_paper_trading_strategy.engine.db import run_with_retry from indian_paper_trading_strategy.engine.time_utils import compute_logical_time def _as_float(value): if hasattr(value, "item"): try: return float(value.item()) except Exception: pass if hasattr(value, "iloc"): try: return float(value.iloc[-1]) except Exception: pass return float(value) def _local_tz(): return datetime.now().astimezone().tzinfo def try_execute_sip( now, market_open, sip_interval, sip_amount, sp_price, gd_price, eq_w, gd_w, broker: Broker | None = None, mode: str | None = "LIVE", ): def _op(cur, _conn): if now.tzinfo is None: now_ts = now.replace(tzinfo=_local_tz()) else: now_ts = now event_ts = now_ts log_event("DEBUG_ENTER_TRY_EXECUTE", { "now": now_ts.isoformat(), }, cur=cur, ts=event_ts) state = load_state(mode=mode, cur=cur, for_update=True) force_execute = state.get("last_sip_ts") is None if not market_open: return state, False last = state.get("last_sip_ts") or state.get("last_run") if last and not force_execute: try: last_dt = datetime.fromisoformat(last) except ValueError: last_dt = None if last_dt: if last_dt.tzinfo is None: last_dt = last_dt.replace(tzinfo=_local_tz()) if now_ts.tzinfo and last_dt.tzinfo and last_dt.tzinfo != now_ts.tzinfo: last_dt = last_dt.astimezone(now_ts.tzinfo) if last_dt and (now_ts - last_dt).total_seconds() < sip_interval: return state, False logical_time = compute_logical_time(now_ts, last, sip_interval) if event_exists("SIP_EXECUTED", logical_time, cur=cur): return state, False sp_price_val = _as_float(sp_price) gd_price_val = _as_float(gd_price) eq_w_val = _as_float(eq_w) gd_w_val = _as_float(gd_w) sip_amount_val = _as_float(sip_amount) nifty_qty = (sip_amount_val * eq_w_val) / sp_price_val gold_qty = (sip_amount_val * gd_w_val) / gd_price_val if broker is None: return state, False funds = broker.get_funds(cur=cur) cash = funds.get("cash") if cash is not None and float(cash) < sip_amount_val: return state, False log_event("DEBUG_EXECUTION_DECISION", { "force_execute": force_execute, "last_sip_ts": state.get("last_sip_ts"), "now": now_ts.isoformat(), }, cur=cur, ts=event_ts) nifty_order = broker.place_order( "NIFTYBEES.NS", "BUY", nifty_qty, sp_price_val, cur=cur, logical_time=logical_time, ) gold_order = broker.place_order( "GOLDBEES.NS", "BUY", gold_qty, gd_price_val, cur=cur, logical_time=logical_time, ) orders = [nifty_order, gold_order] executed = all( isinstance(order, dict) and order.get("status") == "FILLED" for order in orders ) if not executed: return state, False assert len(orders) > 0, "executed=True but no broker orders placed" funds_after = broker.get_funds(cur=cur) cash_after = funds_after.get("cash") if cash_after is not None: state["cash"] = float(cash_after) state["nifty_units"] += nifty_qty state["gold_units"] += gold_qty state["total_invested"] += sip_amount_val state["last_sip_ts"] = now_ts.isoformat() state["last_run"] = now_ts.isoformat() save_state( state, mode=mode, cur=cur, emit_event=True, event_meta={"source": "sip"}, ) log_event( "SIP_EXECUTED", { "nifty_units": nifty_qty, "gold_units": gold_qty, "nifty_price": sp_price_val, "gold_price": gd_price_val, "amount": sip_amount_val, }, cur=cur, ts=event_ts, logical_time=logical_time, ) return state, True return run_with_retry(_op)