2026-02-01 13:57:30 +00:00

155 lines
4.3 KiB
Python

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