2026-02-01 20:34:57 +00:00

158 lines
4.8 KiB
Python

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