from datetime import datetime, timezone from pathlib import Path from indian_paper_trading_strategy.engine.db import db_connection, insert_engine_event, run_with_retry, get_context from indian_paper_trading_strategy.engine.time_utils import normalize_logical_time ENGINE_ROOT = Path(__file__).resolve().parents[1] STORAGE_DIR = ENGINE_ROOT / "storage" MTM_FILE = STORAGE_DIR / "mtm_ledger.csv" MTM_INTERVAL_SECONDS = 60 def _log_mtm_in_tx( cur, nifty_units, gold_units, nifty_price, gold_price, total_invested, ts, logical_time=None, user_id: str | None = None, run_id: str | None = None, ): scope_user, scope_run = get_context(user_id, run_id) logical_ts = normalize_logical_time(logical_time or ts) nifty_value = nifty_units * nifty_price gold_value = gold_units * gold_price portfolio_value = nifty_value + gold_value pnl = portfolio_value - total_invested row = { "timestamp": ts.isoformat(), "logical_time": logical_ts.isoformat(), "nifty_units": nifty_units, "gold_units": gold_units, "nifty_price": nifty_price, "gold_price": gold_price, "nifty_value": nifty_value, "gold_value": gold_value, "portfolio_value": portfolio_value, "total_invested": total_invested, "pnl": pnl, } cur.execute( """ INSERT INTO mtm_ledger ( user_id, run_id, timestamp, logical_time, nifty_units, gold_units, nifty_price, gold_price, nifty_value, gold_value, portfolio_value, total_invested, pnl ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) ON CONFLICT DO NOTHING """, ( scope_user, scope_run, ts, logical_ts, row["nifty_units"], row["gold_units"], row["nifty_price"], row["gold_price"], row["nifty_value"], row["gold_value"], row["portfolio_value"], row["total_invested"], row["pnl"], ), ) if cur.rowcount: insert_engine_event(cur, "MTM_UPDATED", data=row, ts=ts) return portfolio_value, pnl def log_mtm( nifty_units, gold_units, nifty_price, gold_price, total_invested, *, cur=None, logical_time=None, user_id: str | None = None, run_id: str | None = None, ): ts = logical_time or datetime.now(timezone.utc) if cur is not None: return _log_mtm_in_tx( cur, nifty_units, gold_units, nifty_price, gold_price, total_invested, ts, logical_time=logical_time, user_id=user_id, run_id=run_id, ) def _op(cur, _conn): return _log_mtm_in_tx( cur, nifty_units, gold_units, nifty_price, gold_price, total_invested, ts, logical_time=logical_time, user_id=user_id, run_id=run_id, ) return run_with_retry(_op) def _get_last_mtm_ts(user_id: str | None = None, run_id: str | None = None): scope_user, scope_run = get_context(user_id, run_id) with db_connection() as conn: with conn.cursor() as cur: cur.execute( "SELECT MAX(timestamp) FROM mtm_ledger WHERE user_id = %s AND run_id = %s", (scope_user, scope_run), ) row = cur.fetchone() if not row or row[0] is None: return None return row[0].astimezone().replace(tzinfo=None) def should_log_mtm(df, now, user_id: str | None = None, run_id: str | None = None): if df is None: last_ts = _get_last_mtm_ts(user_id=user_id, run_id=run_id) if last_ts is None: return True return (now - last_ts).total_seconds() >= MTM_INTERVAL_SECONDS if getattr(df, "empty", False): return True try: last_ts = datetime.fromisoformat(str(df.iloc[-1]["timestamp"])) except Exception: return True return (now - last_ts).total_seconds() >= MTM_INTERVAL_SECONDS