Fixed Errors

This commit is contained in:
root 2026-02-01 20:34:57 +00:00
parent 82098ff9e5
commit db7315911b
38 changed files with 6545 additions and 2578 deletions

View File

@ -1,208 +1,208 @@
import streamlit as st
import time
from datetime import datetime
from pathlib import Path
import sys
import pandas as pd
PROJECT_ROOT = Path(__file__).resolve().parents[2]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from indian_paper_trading_strategy.engine.history import load_monthly_close
from indian_paper_trading_strategy.engine.market import india_market_status
from indian_paper_trading_strategy.engine.data import fetch_live_price
from indian_paper_trading_strategy.engine.strategy import allocation
from indian_paper_trading_strategy.engine.execution import try_execute_sip
from indian_paper_trading_strategy.engine.state import load_state, save_state
from indian_paper_trading_strategy.engine.db import db_connection, insert_engine_event, run_with_retry, get_default_user_id, get_active_run_id, set_context
from indian_paper_trading_strategy.engine.mtm import log_mtm, should_log_mtm
_STREAMLIT_USER_ID = get_default_user_id()
_STREAMLIT_RUN_ID = get_active_run_id(_STREAMLIT_USER_ID) if _STREAMLIT_USER_ID else None
if _STREAMLIT_USER_ID and _STREAMLIT_RUN_ID:
set_context(_STREAMLIT_USER_ID, _STREAMLIT_RUN_ID)
def reset_runtime_state():
def _op(cur, _conn):
cur.execute(
"DELETE FROM mtm_ledger WHERE user_id = %s AND run_id = %s",
(_STREAMLIT_USER_ID, _STREAMLIT_RUN_ID),
)
cur.execute(
"DELETE FROM event_ledger WHERE user_id = %s AND run_id = %s",
(_STREAMLIT_USER_ID, _STREAMLIT_RUN_ID),
)
cur.execute(
"DELETE FROM engine_state WHERE user_id = %s AND run_id = %s",
(_STREAMLIT_USER_ID, _STREAMLIT_RUN_ID),
)
insert_engine_event(cur, "LIVE_RESET", data={})
run_with_retry(_op)
def load_mtm_df():
with db_connection() as conn:
return pd.read_sql_query(
"SELECT timestamp, pnl FROM mtm_ledger WHERE user_id = %s AND run_id = %s ORDER BY timestamp",
conn,
params=(_STREAMLIT_USER_ID, _STREAMLIT_RUN_ID),
)
def is_engine_running():
state = load_state(mode="LIVE")
return state.get("total_invested", 0) > 0 or \
state.get("nifty_units", 0) > 0 or \
state.get("gold_units", 0) > 0
if "engine_active" not in st.session_state:
st.session_state.engine_active = is_engine_running()
NIFTY = "NIFTYBEES.NS"
GOLD = "GOLDBEES.NS"
SMA_MONTHS = 36
def get_prices():
try:
nifty = fetch_live_price(NIFTY)
gold = fetch_live_price(GOLD)
return nifty, gold
except Exception as e:
st.error(e)
return None, None
SIP_AMOUNT = st.number_input("SIP Amount (\u20B9)", 500, 100000, 5000)
SIP_INTERVAL_SEC = st.number_input("SIP Interval (sec) [TEST]", 30, 3600, 120)
REFRESH_SEC = st.slider("Refresh interval (sec)", 5, 60, 10)
st.title("SIPXAR INDIA - Phase-1 Safe Engine")
market_open, market_time = india_market_status()
st.info(f"NSE Market {'OPEN' if market_open else 'CLOSED'} | IST {market_time}")
if not market_open:
st.info("Market is closed. Portfolio values are frozen at last available prices.")
col1, col2 = st.columns(2)
with col1:
if st.button("START ENGINE"):
if is_engine_running():
st.info("Engine already running. Resuming.")
st.session_state.engine_active = True
else:
st.session_state.engine_active = True
# HARD RESET ONLY ON FIRST START
reset_runtime_state()
save_state({
"total_invested": 0.0,
"nifty_units": 0.0,
"gold_units": 0.0,
"last_sip_ts": None,
}, mode="LIVE", emit_event=True, event_meta={"source": "streamlit_start"})
st.success("Engine started")
with col2:
if st.button("KILL ENGINE"):
st.session_state.engine_active = False
reset_runtime_state()
st.warning("Engine killed and state wiped")
st.stop()
if not st.session_state.engine_active:
st.stop()
state = load_state(mode="LIVE")
nifty_price, gold_price = get_prices()
if nifty_price is None:
st.stop()
st.subheader("Latest Market Prices (LTP)")
c1, c2 = st.columns(2)
with c1:
st.metric(
label="NIFTYBEES",
value=f"\u20B9{nifty_price:,.2f}",
help="Last traded price (delayed)"
)
with c2:
st.metric(
label="GOLDBEES",
value=f"\u20B9{gold_price:,.2f}",
help="Last traded price (delayed)"
)
st.caption(f"Price timestamp: {datetime.now().strftime('%H:%M:%S')}")
nifty_hist = load_monthly_close(NIFTY)
gold_hist = load_monthly_close(GOLD)
nifty_sma = nifty_hist.rolling(SMA_MONTHS).mean().iloc[-1]
gold_sma = gold_hist.rolling(SMA_MONTHS).mean().iloc[-1]
eq_w, gd_w = allocation(
sp_price=nifty_price,
gd_price=gold_price,
sp_sma=nifty_sma,
gd_sma=gold_sma
)
state, executed = try_execute_sip(
now=datetime.now(),
market_open=market_open,
sip_interval=SIP_INTERVAL_SEC,
sip_amount=SIP_AMOUNT,
sp_price=nifty_price,
gd_price=gold_price,
eq_w=eq_w,
gd_w=gd_w,
mode="LIVE",
)
now = datetime.now()
if market_open and should_log_mtm(None, now):
portfolio_value, pnl = log_mtm(
nifty_units=state["nifty_units"],
gold_units=state["gold_units"],
nifty_price=nifty_price,
gold_price=gold_price,
total_invested=state["total_invested"],
)
else:
# Market closed -> freeze valuation (do NOT log)
portfolio_value = (
state["nifty_units"] * nifty_price +
state["gold_units"] * gold_price
)
pnl = portfolio_value - state["total_invested"]
st.subheader("Equity Curve (Unrealized PnL)")
mtm_df = load_mtm_df()
if "timestamp" in mtm_df.columns and "pnl" in mtm_df.columns and len(mtm_df) > 1:
mtm_df["timestamp"] = pd.to_datetime(mtm_df["timestamp"])
mtm_df = mtm_df.sort_values("timestamp").set_index("timestamp")
st.line_chart(mtm_df["pnl"], height=350)
else:
st.warning("Not enough MTM data or missing columns. Expected: timestamp, pnl.")
st.metric("Total Invested", f"\u20B9{state['total_invested']:,.0f}")
st.metric("NIFTY Units", round(state["nifty_units"], 4))
st.metric("Gold Units", round(state["gold_units"], 4))
st.metric("Portfolio Value", f"\u20B9{portfolio_value:,.0f}")
st.metric("PnL", f"\u20B9{pnl:,.0f}")
time.sleep(REFRESH_SEC)
st.rerun()
import streamlit as st
import time
from datetime import datetime
from pathlib import Path
import sys
import pandas as pd
PROJECT_ROOT = Path(__file__).resolve().parents[2]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from indian_paper_trading_strategy.engine.history import load_monthly_close
from indian_paper_trading_strategy.engine.market import india_market_status
from indian_paper_trading_strategy.engine.data import fetch_live_price
from indian_paper_trading_strategy.engine.strategy import allocation
from indian_paper_trading_strategy.engine.execution import try_execute_sip
from indian_paper_trading_strategy.engine.state import load_state, save_state
from indian_paper_trading_strategy.engine.db import db_connection, insert_engine_event, run_with_retry, get_default_user_id, get_active_run_id, set_context
from indian_paper_trading_strategy.engine.mtm import log_mtm, should_log_mtm
_STREAMLIT_USER_ID = get_default_user_id()
_STREAMLIT_RUN_ID = get_active_run_id(_STREAMLIT_USER_ID) if _STREAMLIT_USER_ID else None
if _STREAMLIT_USER_ID and _STREAMLIT_RUN_ID:
set_context(_STREAMLIT_USER_ID, _STREAMLIT_RUN_ID)
def reset_runtime_state():
def _op(cur, _conn):
cur.execute(
"DELETE FROM mtm_ledger WHERE user_id = %s AND run_id = %s",
(_STREAMLIT_USER_ID, _STREAMLIT_RUN_ID),
)
cur.execute(
"DELETE FROM event_ledger WHERE user_id = %s AND run_id = %s",
(_STREAMLIT_USER_ID, _STREAMLIT_RUN_ID),
)
cur.execute(
"DELETE FROM engine_state WHERE user_id = %s AND run_id = %s",
(_STREAMLIT_USER_ID, _STREAMLIT_RUN_ID),
)
insert_engine_event(cur, "LIVE_RESET", data={})
run_with_retry(_op)
def load_mtm_df():
with db_connection() as conn:
return pd.read_sql_query(
"SELECT timestamp, pnl FROM mtm_ledger WHERE user_id = %s AND run_id = %s ORDER BY timestamp",
conn,
params=(_STREAMLIT_USER_ID, _STREAMLIT_RUN_ID),
)
def is_engine_running():
state = load_state(mode="LIVE")
return state.get("total_invested", 0) > 0 or \
state.get("nifty_units", 0) > 0 or \
state.get("gold_units", 0) > 0
if "engine_active" not in st.session_state:
st.session_state.engine_active = is_engine_running()
NIFTY = "NIFTYBEES.NS"
GOLD = "GOLDBEES.NS"
SMA_MONTHS = 36
def get_prices():
try:
nifty = fetch_live_price(NIFTY)
gold = fetch_live_price(GOLD)
return nifty, gold
except Exception as e:
st.error(e)
return None, None
SIP_AMOUNT = st.number_input("SIP Amount (\u20B9)", 500, 100000, 5000)
SIP_INTERVAL_SEC = st.number_input("SIP Interval (sec) [TEST]", 30, 3600, 120)
REFRESH_SEC = st.slider("Refresh interval (sec)", 5, 60, 10)
st.title("SIPXAR INDIA - Phase-1 Safe Engine")
market_open, market_time = india_market_status()
st.info(f"NSE Market {'OPEN' if market_open else 'CLOSED'} | IST {market_time}")
if not market_open:
st.info("Market is closed. Portfolio values are frozen at last available prices.")
col1, col2 = st.columns(2)
with col1:
if st.button("START ENGINE"):
if is_engine_running():
st.info("Engine already running. Resuming.")
st.session_state.engine_active = True
else:
st.session_state.engine_active = True
# HARD RESET ONLY ON FIRST START
reset_runtime_state()
save_state({
"total_invested": 0.0,
"nifty_units": 0.0,
"gold_units": 0.0,
"last_sip_ts": None,
}, mode="LIVE", emit_event=True, event_meta={"source": "streamlit_start"})
st.success("Engine started")
with col2:
if st.button("KILL ENGINE"):
st.session_state.engine_active = False
reset_runtime_state()
st.warning("Engine killed and state wiped")
st.stop()
if not st.session_state.engine_active:
st.stop()
state = load_state(mode="LIVE")
nifty_price, gold_price = get_prices()
if nifty_price is None:
st.stop()
st.subheader("Latest Market Prices (LTP)")
c1, c2 = st.columns(2)
with c1:
st.metric(
label="NIFTYBEES",
value=f"\u20B9{nifty_price:,.2f}",
help="Last traded price (delayed)"
)
with c2:
st.metric(
label="GOLDBEES",
value=f"\u20B9{gold_price:,.2f}",
help="Last traded price (delayed)"
)
st.caption(f"Price timestamp: {datetime.now().strftime('%H:%M:%S')}")
nifty_hist = load_monthly_close(NIFTY)
gold_hist = load_monthly_close(GOLD)
nifty_sma = nifty_hist.rolling(SMA_MONTHS).mean().iloc[-1]
gold_sma = gold_hist.rolling(SMA_MONTHS).mean().iloc[-1]
eq_w, gd_w = allocation(
sp_price=nifty_price,
gd_price=gold_price,
sp_sma=nifty_sma,
gd_sma=gold_sma
)
state, executed = try_execute_sip(
now=datetime.now(),
market_open=market_open,
sip_interval=SIP_INTERVAL_SEC,
sip_amount=SIP_AMOUNT,
sp_price=nifty_price,
gd_price=gold_price,
eq_w=eq_w,
gd_w=gd_w,
mode="LIVE",
)
now = datetime.now()
if market_open and should_log_mtm(None, now):
portfolio_value, pnl = log_mtm(
nifty_units=state["nifty_units"],
gold_units=state["gold_units"],
nifty_price=nifty_price,
gold_price=gold_price,
total_invested=state["total_invested"],
)
else:
# Market closed -> freeze valuation (do NOT log)
portfolio_value = (
state["nifty_units"] * nifty_price +
state["gold_units"] * gold_price
)
pnl = portfolio_value - state["total_invested"]
st.subheader("Equity Curve (Unrealized PnL)")
mtm_df = load_mtm_df()
if "timestamp" in mtm_df.columns and "pnl" in mtm_df.columns and len(mtm_df) > 1:
mtm_df["timestamp"] = pd.to_datetime(mtm_df["timestamp"])
mtm_df = mtm_df.sort_values("timestamp").set_index("timestamp")
st.line_chart(mtm_df["pnl"], height=350)
else:
st.warning("Not enough MTM data or missing columns. Expected: timestamp, pnl.")
st.metric("Total Invested", f"\u20B9{state['total_invested']:,.0f}")
st.metric("NIFTY Units", round(state["nifty_units"], 4))
st.metric("Gold Units", round(state["gold_units"], 4))
st.metric("Portfolio Value", f"\u20B9{portfolio_value:,.0f}")
st.metric("PnL", f"\u20B9{pnl:,.0f}")
time.sleep(REFRESH_SEC)
st.rerun()

View File

@ -1 +1 @@
"""Engine package for the India paper trading strategy."""
"""Engine package for the India paper trading strategy."""

File diff suppressed because it is too large Load Diff

View File

@ -1,150 +1,150 @@
import json
from datetime import datetime
from indian_paper_trading_strategy.engine.db import db_connection, get_context
DEFAULT_CONFIG = {
"active": False,
"sip_amount": 0,
"sip_frequency": {"value": 30, "unit": "days"},
"next_run": None
}
def _maybe_parse_json(value):
if value is None:
return None
if not isinstance(value, str):
return value
text = value.strip()
if not text:
return None
try:
return json.loads(text)
except Exception:
return value
def _format_ts(value: datetime | None):
if value is None:
return None
return value.isoformat()
def load_strategy_config(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 strategy, sip_amount, sip_frequency_value, sip_frequency_unit,
mode, broker, active, frequency, frequency_days, unit, next_run
FROM strategy_config
WHERE user_id = %s AND run_id = %s
LIMIT 1
""",
(scope_user, scope_run),
)
row = cur.fetchone()
if not row:
return DEFAULT_CONFIG.copy()
cfg = DEFAULT_CONFIG.copy()
cfg["strategy"] = row[0]
cfg["strategy_name"] = row[0]
cfg["sip_amount"] = float(row[1]) if row[1] is not None else cfg.get("sip_amount")
cfg["mode"] = row[4]
cfg["broker"] = row[5]
cfg["active"] = row[6] if row[6] is not None else cfg.get("active")
cfg["frequency"] = _maybe_parse_json(row[7])
cfg["frequency_days"] = row[8]
cfg["unit"] = row[9]
cfg["next_run"] = _format_ts(row[10])
if row[2] is not None or row[3] is not None:
cfg["sip_frequency"] = {"value": row[2], "unit": row[3]}
else:
value = cfg.get("frequency")
unit = cfg.get("unit")
if isinstance(value, dict):
unit = value.get("unit", unit)
value = value.get("value")
if value is None and cfg.get("frequency_days") is not None:
value = cfg.get("frequency_days")
unit = unit or "days"
if value is not None and unit:
cfg["sip_frequency"] = {"value": value, "unit": unit}
return cfg
def save_strategy_config(cfg, user_id: str | None = None, run_id: str | None = None):
scope_user, scope_run = get_context(user_id, run_id)
sip_frequency = cfg.get("sip_frequency")
sip_value = None
sip_unit = None
if isinstance(sip_frequency, dict):
sip_value = sip_frequency.get("value")
sip_unit = sip_frequency.get("unit")
frequency = cfg.get("frequency")
if not isinstance(frequency, str) and frequency is not None:
frequency = json.dumps(frequency)
next_run = cfg.get("next_run")
next_run_dt = None
if isinstance(next_run, str):
try:
next_run_dt = datetime.fromisoformat(next_run)
except ValueError:
next_run_dt = None
strategy = cfg.get("strategy") or cfg.get("strategy_name")
with db_connection() as conn:
with conn:
with conn.cursor() as cur:
cur.execute(
"""
INSERT INTO strategy_config (
user_id,
run_id,
strategy,
sip_amount,
sip_frequency_value,
sip_frequency_unit,
mode,
broker,
active,
frequency,
frequency_days,
unit,
next_run
)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (user_id, run_id) DO UPDATE
SET strategy = EXCLUDED.strategy,
sip_amount = EXCLUDED.sip_amount,
sip_frequency_value = EXCLUDED.sip_frequency_value,
sip_frequency_unit = EXCLUDED.sip_frequency_unit,
mode = EXCLUDED.mode,
broker = EXCLUDED.broker,
active = EXCLUDED.active,
frequency = EXCLUDED.frequency,
frequency_days = EXCLUDED.frequency_days,
unit = EXCLUDED.unit,
next_run = EXCLUDED.next_run
""",
(
scope_user,
scope_run,
strategy,
cfg.get("sip_amount"),
sip_value,
sip_unit,
cfg.get("mode"),
cfg.get("broker"),
cfg.get("active"),
frequency,
cfg.get("frequency_days"),
cfg.get("unit"),
next_run_dt,
),
)
import json
from datetime import datetime
from indian_paper_trading_strategy.engine.db import db_connection, get_context
DEFAULT_CONFIG = {
"active": False,
"sip_amount": 0,
"sip_frequency": {"value": 30, "unit": "days"},
"next_run": None
}
def _maybe_parse_json(value):
if value is None:
return None
if not isinstance(value, str):
return value
text = value.strip()
if not text:
return None
try:
return json.loads(text)
except Exception:
return value
def _format_ts(value: datetime | None):
if value is None:
return None
return value.isoformat()
def load_strategy_config(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 strategy, sip_amount, sip_frequency_value, sip_frequency_unit,
mode, broker, active, frequency, frequency_days, unit, next_run
FROM strategy_config
WHERE user_id = %s AND run_id = %s
LIMIT 1
""",
(scope_user, scope_run),
)
row = cur.fetchone()
if not row:
return DEFAULT_CONFIG.copy()
cfg = DEFAULT_CONFIG.copy()
cfg["strategy"] = row[0]
cfg["strategy_name"] = row[0]
cfg["sip_amount"] = float(row[1]) if row[1] is not None else cfg.get("sip_amount")
cfg["mode"] = row[4]
cfg["broker"] = row[5]
cfg["active"] = row[6] if row[6] is not None else cfg.get("active")
cfg["frequency"] = _maybe_parse_json(row[7])
cfg["frequency_days"] = row[8]
cfg["unit"] = row[9]
cfg["next_run"] = _format_ts(row[10])
if row[2] is not None or row[3] is not None:
cfg["sip_frequency"] = {"value": row[2], "unit": row[3]}
else:
value = cfg.get("frequency")
unit = cfg.get("unit")
if isinstance(value, dict):
unit = value.get("unit", unit)
value = value.get("value")
if value is None and cfg.get("frequency_days") is not None:
value = cfg.get("frequency_days")
unit = unit or "days"
if value is not None and unit:
cfg["sip_frequency"] = {"value": value, "unit": unit}
return cfg
def save_strategy_config(cfg, user_id: str | None = None, run_id: str | None = None):
scope_user, scope_run = get_context(user_id, run_id)
sip_frequency = cfg.get("sip_frequency")
sip_value = None
sip_unit = None
if isinstance(sip_frequency, dict):
sip_value = sip_frequency.get("value")
sip_unit = sip_frequency.get("unit")
frequency = cfg.get("frequency")
if not isinstance(frequency, str) and frequency is not None:
frequency = json.dumps(frequency)
next_run = cfg.get("next_run")
next_run_dt = None
if isinstance(next_run, str):
try:
next_run_dt = datetime.fromisoformat(next_run)
except ValueError:
next_run_dt = None
strategy = cfg.get("strategy") or cfg.get("strategy_name")
with db_connection() as conn:
with conn:
with conn.cursor() as cur:
cur.execute(
"""
INSERT INTO strategy_config (
user_id,
run_id,
strategy,
sip_amount,
sip_frequency_value,
sip_frequency_unit,
mode,
broker,
active,
frequency,
frequency_days,
unit,
next_run
)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (user_id, run_id) DO UPDATE
SET strategy = EXCLUDED.strategy,
sip_amount = EXCLUDED.sip_amount,
sip_frequency_value = EXCLUDED.sip_frequency_value,
sip_frequency_unit = EXCLUDED.sip_frequency_unit,
mode = EXCLUDED.mode,
broker = EXCLUDED.broker,
active = EXCLUDED.active,
frequency = EXCLUDED.frequency,
frequency_days = EXCLUDED.frequency_days,
unit = EXCLUDED.unit,
next_run = EXCLUDED.next_run
""",
(
scope_user,
scope_run,
strategy,
cfg.get("sip_amount"),
sip_value,
sip_unit,
cfg.get("mode"),
cfg.get("broker"),
cfg.get("active"),
frequency,
cfg.get("frequency_days"),
cfg.get("unit"),
next_run_dt,
),
)

View File

@ -1,81 +1,81 @@
# engine/data.py
from datetime import datetime, timezone
from pathlib import Path
import os
import threading
import pandas as pd
import yfinance as yf
ENGINE_ROOT = Path(__file__).resolve().parents[1]
HISTORY_DIR = ENGINE_ROOT / "storage" / "history"
ALLOW_PRICE_CACHE = os.getenv("ALLOW_PRICE_CACHE", "0").strip().lower() in {"1", "true", "yes"}
_LAST_PRICE: dict[str, dict[str, object]] = {}
_LAST_PRICE_LOCK = threading.Lock()
def _set_last_price(ticker: str, price: float, source: str):
now = datetime.now(timezone.utc)
with _LAST_PRICE_LOCK:
_LAST_PRICE[ticker] = {"price": float(price), "source": source, "ts": now}
def get_price_snapshot(ticker: str) -> dict[str, object] | None:
with _LAST_PRICE_LOCK:
data = _LAST_PRICE.get(ticker)
if not data:
return None
return dict(data)
def _get_last_live_price(ticker: str) -> float | None:
with _LAST_PRICE_LOCK:
data = _LAST_PRICE.get(ticker)
if not data:
return None
if data.get("source") == "live":
return float(data.get("price", 0))
return None
def _cached_last_close(ticker: str) -> float | None:
file = HISTORY_DIR / f"{ticker}.csv"
if not file.exists():
return None
df = pd.read_csv(file)
if df.empty or "Close" not in df.columns:
return None
return float(df["Close"].iloc[-1])
def fetch_live_price(ticker, allow_cache: bool | None = None):
if allow_cache is None:
allow_cache = ALLOW_PRICE_CACHE
try:
df = yf.download(
ticker,
period="1d",
interval="1m",
auto_adjust=True,
progress=False,
timeout=5,
)
if df is not None and not df.empty:
price = float(df["Close"].iloc[-1])
_set_last_price(ticker, price, "live")
return price
except Exception:
pass
if allow_cache:
last_live = _get_last_live_price(ticker)
if last_live is not None:
return last_live
cached = _cached_last_close(ticker)
if cached is not None:
_set_last_price(ticker, cached, "cache")
return cached
raise RuntimeError(f"No live data for {ticker}")
# engine/data.py
from datetime import datetime, timezone
from pathlib import Path
import os
import threading
import pandas as pd
import yfinance as yf
ENGINE_ROOT = Path(__file__).resolve().parents[1]
HISTORY_DIR = ENGINE_ROOT / "storage" / "history"
ALLOW_PRICE_CACHE = os.getenv("ALLOW_PRICE_CACHE", "0").strip().lower() in {"1", "true", "yes"}
_LAST_PRICE: dict[str, dict[str, object]] = {}
_LAST_PRICE_LOCK = threading.Lock()
def _set_last_price(ticker: str, price: float, source: str):
now = datetime.now(timezone.utc)
with _LAST_PRICE_LOCK:
_LAST_PRICE[ticker] = {"price": float(price), "source": source, "ts": now}
def get_price_snapshot(ticker: str) -> dict[str, object] | None:
with _LAST_PRICE_LOCK:
data = _LAST_PRICE.get(ticker)
if not data:
return None
return dict(data)
def _get_last_live_price(ticker: str) -> float | None:
with _LAST_PRICE_LOCK:
data = _LAST_PRICE.get(ticker)
if not data:
return None
if data.get("source") == "live":
return float(data.get("price", 0))
return None
def _cached_last_close(ticker: str) -> float | None:
file = HISTORY_DIR / f"{ticker}.csv"
if not file.exists():
return None
df = pd.read_csv(file)
if df.empty or "Close" not in df.columns:
return None
return float(df["Close"].iloc[-1])
def fetch_live_price(ticker, allow_cache: bool | None = None):
if allow_cache is None:
allow_cache = ALLOW_PRICE_CACHE
try:
df = yf.download(
ticker,
period="1d",
interval="1m",
auto_adjust=True,
progress=False,
timeout=5,
)
if df is not None and not df.empty:
price = float(df["Close"].iloc[-1])
_set_last_price(ticker, price, "live")
return price
except Exception:
pass
if allow_cache:
last_live = _get_last_live_price(ticker)
if last_live is not None:
return last_live
cached = _cached_last_close(ticker)
if cached is not None:
_set_last_price(ticker, cached, "cache")
return cached
raise RuntimeError(f"No live data for {ticker}")

View File

@ -0,0 +1,324 @@
import os
import threading
import time
from contextlib import contextmanager
from datetime import datetime, timezone
from contextvars import ContextVar
import psycopg2
from psycopg2 import pool
from psycopg2 import OperationalError, InterfaceError
from psycopg2.extras import Json
_POOL = None
_POOL_LOCK = threading.Lock()
_DEFAULT_USER_ID = None
_DEFAULT_LOCK = threading.Lock()
_USER_ID = ContextVar("engine_user_id", default=None)
_RUN_ID = ContextVar("engine_run_id", default=None)
def _db_config():
url = os.getenv("DATABASE_URL")
if url:
return {"dsn": url}
schema = os.getenv("DB_SCHEMA") or os.getenv("PGSCHEMA") or "quant_app"
return {
"host": os.getenv("DB_HOST") or os.getenv("PGHOST") or "localhost",
"port": int(os.getenv("DB_PORT") or os.getenv("PGPORT") or "5432"),
"dbname": os.getenv("DB_NAME") or os.getenv("PGDATABASE") or "trading_db",
"user": os.getenv("DB_USER") or os.getenv("PGUSER") or "trader",
"password": os.getenv("DB_PASSWORD") or os.getenv("PGPASSWORD") or "traderpass",
"connect_timeout": int(os.getenv("DB_CONNECT_TIMEOUT", "5")),
"options": f"-csearch_path={schema},public" if schema else None,
}
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"))
if "dsn" in config:
return pool.ThreadedConnectionPool(minconn, maxconn, dsn=config["dsn"])
return pool.ThreadedConnectionPool(minconn, maxconn, **config)
def get_pool():
global _POOL
if _POOL is None:
with _POOL_LOCK:
if _POOL is None:
_POOL = _init_pool()
return _POOL
def _get_connection():
return get_pool().getconn()
def _put_connection(conn, close=False):
try:
get_pool().putconn(conn, close=close)
except Exception:
try:
conn.close()
except Exception:
pass
@contextmanager
def db_connection(retries: int | None = None, delay: float | None = None):
attempts = retries if retries is not None else int(os.getenv("DB_RETRY_COUNT", "3"))
backoff = delay if delay is not None else float(os.getenv("DB_RETRY_DELAY", "0.2"))
last_error = None
for attempt in range(attempts):
conn = None
try:
conn = _get_connection()
conn.autocommit = False
yield conn
return
except (OperationalError, InterfaceError) as exc:
last_error = exc
if conn is not None:
_put_connection(conn, close=True)
conn = None
time.sleep(backoff * (2 ** attempt))
continue
finally:
if conn is not None:
_put_connection(conn, close=conn.closed != 0)
if last_error:
raise last_error
def run_with_retry(operation, retries: int | None = None, delay: float | None = None):
attempts = retries if retries is not None else int(os.getenv("DB_RETRY_COUNT", "3"))
backoff = delay if delay is not None else float(os.getenv("DB_RETRY_DELAY", "0.2"))
last_error = None
for attempt in range(attempts):
with db_connection(retries=1) as conn:
try:
with conn.cursor() as cur:
result = operation(cur, conn)
conn.commit()
return result
except (OperationalError, InterfaceError) as exc:
conn.rollback()
last_error = exc
time.sleep(backoff * (2 ** attempt))
continue
except Exception:
conn.rollback()
raise
if last_error:
raise last_error
@contextmanager
def db_transaction():
with db_connection() as conn:
try:
with conn.cursor() as cur:
yield cur
conn.commit()
except Exception:
conn.rollback()
raise
def _utc_now():
return datetime.utcnow().replace(tzinfo=timezone.utc)
def set_context(user_id: str | None, run_id: str | None):
token_user = _USER_ID.set(user_id)
token_run = _RUN_ID.set(run_id)
return token_user, token_run
def reset_context(token_user, token_run):
_USER_ID.reset(token_user)
_RUN_ID.reset(token_run)
@contextmanager
def engine_context(user_id: str, run_id: str):
token_user, token_run = set_context(user_id, run_id)
try:
yield
finally:
reset_context(token_user, token_run)
def _resolve_context(user_id: str | None = None, run_id: str | None = None):
ctx_user = user_id or _USER_ID.get()
ctx_run = run_id or _RUN_ID.get()
if ctx_user and ctx_run:
return ctx_user, ctx_run
env_user = os.getenv("ENGINE_USER_ID")
env_run = os.getenv("ENGINE_RUN_ID")
if not ctx_user and env_user:
ctx_user = env_user
if not ctx_run and env_run:
ctx_run = env_run
if ctx_user and ctx_run:
return ctx_user, ctx_run
if not ctx_user:
ctx_user = get_default_user_id()
if ctx_user and not ctx_run:
ctx_run = get_active_run_id(ctx_user)
if not ctx_user or not ctx_run:
raise ValueError("engine context missing user_id/run_id")
return ctx_user, ctx_run
def get_context(user_id: str | None = None, run_id: str | None = None):
return _resolve_context(user_id, run_id)
def get_default_user_id():
global _DEFAULT_USER_ID
if _DEFAULT_USER_ID:
return _DEFAULT_USER_ID
def _op(cur, _conn):
cur.execute("SELECT id FROM app_user ORDER BY username LIMIT 1")
row = cur.fetchone()
return row[0] if row else None
user_id = run_with_retry(_op)
if user_id:
with _DEFAULT_LOCK:
_DEFAULT_USER_ID = user_id
return user_id
def _default_run_id(user_id: str) -> str:
return f"default_{user_id}"
def ensure_default_run(user_id: str):
run_id = _default_run_id(user_id)
def _op(cur, _conn):
now = _utc_now()
cur.execute(
"""
INSERT INTO strategy_run (
run_id, user_id, created_at, started_at, stopped_at, status, strategy, mode, broker, meta
)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (run_id) DO NOTHING
""",
(
run_id,
user_id,
now,
None,
None,
"STOPPED",
None,
None,
None,
Json({}),
),
)
return run_id
return run_with_retry(_op)
def get_active_run_id(user_id: str):
def _op(cur, _conn):
cur.execute(
"""
SELECT run_id
FROM strategy_run
WHERE user_id = %s AND status = 'RUNNING'
ORDER BY created_at DESC
LIMIT 1
""",
(user_id,),
)
row = cur.fetchone()
if row:
return row[0]
cur.execute(
"""
SELECT run_id
FROM strategy_run
WHERE user_id = %s
ORDER BY created_at DESC
LIMIT 1
""",
(user_id,),
)
row = cur.fetchone()
if row:
return row[0]
return None
run_id = run_with_retry(_op)
if run_id:
return run_id
return ensure_default_run(user_id)
def get_running_runs(user_id: str | None = None):
def _op(cur, _conn):
if user_id:
cur.execute(
"""
SELECT user_id, run_id
FROM strategy_run
WHERE user_id = %s AND status = 'RUNNING'
ORDER BY created_at DESC
""",
(user_id,),
)
else:
cur.execute(
"""
SELECT user_id, run_id
FROM strategy_run
WHERE status = 'RUNNING'
ORDER BY created_at DESC
"""
)
return cur.fetchall()
return run_with_retry(_op)
def insert_engine_event(
cur,
event: str,
data=None,
message: str | None = None,
meta=None,
ts=None,
user_id: str | None = None,
run_id: str | None = None,
):
when = ts or _utc_now()
scope_user, scope_run = _resolve_context(user_id, run_id)
cur.execute(
"""
INSERT INTO engine_event (user_id, run_id, ts, event, data, message, meta)
VALUES (%s, %s, %s, %s, %s, %s, %s)
""",
(
scope_user,
scope_run,
when,
event,
Json(data) if data is not None else None,
message,
Json(meta) if meta is not None else None,
),
)

View File

@ -1,198 +1,198 @@
import time
from datetime import datetime, timezone
from indian_paper_trading_strategy.engine.db import (
run_with_retry,
insert_engine_event,
get_default_user_id,
get_active_run_id,
get_running_runs,
engine_context,
)
def log_event(event: str, data: dict | None = None):
now = datetime.utcnow().replace(tzinfo=timezone.utc)
payload = data or {}
def _op(cur, _conn):
insert_engine_event(cur, event, data=payload, ts=now)
run_with_retry(_op)
def _update_engine_status(user_id: str, run_id: str, status: str):
now = datetime.utcnow().replace(tzinfo=timezone.utc)
def _op(cur, _conn):
cur.execute(
"""
INSERT INTO engine_status (user_id, run_id, status, last_updated)
VALUES (%s, %s, %s, %s)
ON CONFLICT (user_id, run_id) DO UPDATE
SET status = EXCLUDED.status,
last_updated = EXCLUDED.last_updated
""",
(user_id, run_id, status, now),
)
run_with_retry(_op)
from indian_paper_trading_strategy.engine.config import load_strategy_config, save_strategy_config
from indian_paper_trading_strategy.engine.market import india_market_status
from indian_paper_trading_strategy.engine.execution import try_execute_sip
from indian_paper_trading_strategy.engine.state import load_state
from indian_paper_trading_strategy.engine.broker import PaperBroker
from indian_paper_trading_strategy.engine.data import fetch_live_price
from indian_paper_trading_strategy.engine.mtm import log_mtm, should_log_mtm
from indian_paper_trading_strategy.engine.history import load_monthly_close
from indian_paper_trading_strategy.engine.strategy import allocation
from indian_paper_trading_strategy.engine.time_utils import frequency_to_timedelta, normalize_logical_time
NIFTY = "NIFTYBEES.NS"
GOLD = "GOLDBEES.NS"
SMA_MONTHS = 36
def run_engine(user_id: str | None = None, run_id: str | None = None):
print("Strategy engine started")
active_runs: dict[tuple[str, str], bool] = {}
if run_id and not user_id:
raise ValueError("user_id is required when run_id is provided")
while True:
try:
if user_id and run_id:
runs = [(user_id, run_id)]
elif user_id:
runs = get_running_runs(user_id)
else:
runs = get_running_runs()
if not runs:
default_user = get_default_user_id()
if default_user:
runs = get_running_runs(default_user)
seen = set()
for scope_user, scope_run in runs:
if not scope_user or not scope_run:
continue
seen.add((scope_user, scope_run))
with engine_context(scope_user, scope_run):
cfg = load_strategy_config(user_id=scope_user, run_id=scope_run)
if not cfg.get("active"):
continue
strategy_name = cfg.get("strategy_name", "golden_nifty")
sip_amount = cfg.get("sip_amount", 0)
configured_frequency = cfg.get("sip_frequency") or {}
if not isinstance(configured_frequency, dict):
configured_frequency = {}
frequency_value = int(configured_frequency.get("value", cfg.get("frequency", 0)))
frequency_unit = configured_frequency.get("unit", cfg.get("unit", "days"))
frequency_info = {"value": frequency_value, "unit": frequency_unit}
frequency_label = f"{frequency_value} {frequency_unit}"
if not active_runs.get((scope_user, scope_run)):
log_event(
"ENGINE_START",
{
"strategy": strategy_name,
"sip_amount": sip_amount,
"frequency": frequency_label,
},
)
active_runs[(scope_user, scope_run)] = True
_update_engine_status(scope_user, scope_run, "RUNNING")
market_open, _ = india_market_status()
if not market_open:
log_event("MARKET_CLOSED", {"reason": "Outside market hours"})
continue
now = datetime.now()
mode = (cfg.get("mode") or "PAPER").strip().upper()
if mode not in {"PAPER", "LIVE"}:
mode = "PAPER"
state = load_state(mode=mode)
initial_cash = float(state.get("initial_cash") or 0.0)
broker = PaperBroker(initial_cash=initial_cash) if mode == "PAPER" else None
nifty_price = fetch_live_price(NIFTY)
gold_price = fetch_live_price(GOLD)
next_run = cfg.get("next_run")
if next_run is None or now >= datetime.fromisoformat(next_run):
nifty_hist = load_monthly_close(NIFTY)
gold_hist = load_monthly_close(GOLD)
nifty_sma = nifty_hist.rolling(SMA_MONTHS).mean().iloc[-1]
gold_sma = gold_hist.rolling(SMA_MONTHS).mean().iloc[-1]
eq_w, gd_w = allocation(
sp_price=nifty_price,
gd_price=gold_price,
sp_sma=nifty_sma,
gd_sma=gold_sma,
)
weights = {"equity": eq_w, "gold": gd_w}
state, executed = try_execute_sip(
now=now,
market_open=True,
sip_interval=frequency_to_timedelta(frequency_info).total_seconds(),
sip_amount=sip_amount,
sp_price=nifty_price,
gd_price=gold_price,
eq_w=eq_w,
gd_w=gd_w,
broker=broker,
mode=mode,
)
if executed:
log_event(
"SIP_TRIGGERED",
{
"date": now.date().isoformat(),
"allocation": weights,
"cash_used": sip_amount,
},
)
portfolio_value = (
state["nifty_units"] * nifty_price
+ state["gold_units"] * gold_price
)
log_event(
"PORTFOLIO_UPDATED",
{
"nifty_units": state["nifty_units"],
"gold_units": state["gold_units"],
"portfolio_value": portfolio_value,
},
)
cfg["next_run"] = (now + frequency_to_timedelta(frequency_info)).isoformat()
save_strategy_config(cfg, user_id=scope_user, run_id=scope_run)
if should_log_mtm(None, now):
state = load_state(mode=mode)
log_mtm(
nifty_units=state["nifty_units"],
gold_units=state["gold_units"],
nifty_price=nifty_price,
gold_price=gold_price,
total_invested=state["total_invested"],
logical_time=normalize_logical_time(now),
)
for key in list(active_runs.keys()):
if key not in seen:
active_runs.pop(key, None)
time.sleep(30)
except Exception as e:
log_event("ENGINE_ERROR", {"error": str(e)})
raise
if __name__ == "__main__":
run_engine()
import time
from datetime import datetime, timezone
from indian_paper_trading_strategy.engine.db import (
run_with_retry,
insert_engine_event,
get_default_user_id,
get_active_run_id,
get_running_runs,
engine_context,
)
def log_event(event: str, data: dict | None = None):
now = datetime.utcnow().replace(tzinfo=timezone.utc)
payload = data or {}
def _op(cur, _conn):
insert_engine_event(cur, event, data=payload, ts=now)
run_with_retry(_op)
def _update_engine_status(user_id: str, run_id: str, status: str):
now = datetime.utcnow().replace(tzinfo=timezone.utc)
def _op(cur, _conn):
cur.execute(
"""
INSERT INTO engine_status (user_id, run_id, status, last_updated)
VALUES (%s, %s, %s, %s)
ON CONFLICT (user_id, run_id) DO UPDATE
SET status = EXCLUDED.status,
last_updated = EXCLUDED.last_updated
""",
(user_id, run_id, status, now),
)
run_with_retry(_op)
from indian_paper_trading_strategy.engine.config import load_strategy_config, save_strategy_config
from indian_paper_trading_strategy.engine.market import india_market_status
from indian_paper_trading_strategy.engine.execution import try_execute_sip
from indian_paper_trading_strategy.engine.state import load_state
from indian_paper_trading_strategy.engine.broker import PaperBroker
from indian_paper_trading_strategy.engine.data import fetch_live_price
from indian_paper_trading_strategy.engine.mtm import log_mtm, should_log_mtm
from indian_paper_trading_strategy.engine.history import load_monthly_close
from indian_paper_trading_strategy.engine.strategy import allocation
from indian_paper_trading_strategy.engine.time_utils import frequency_to_timedelta, normalize_logical_time
NIFTY = "NIFTYBEES.NS"
GOLD = "GOLDBEES.NS"
SMA_MONTHS = 36
def run_engine(user_id: str | None = None, run_id: str | None = None):
print("Strategy engine started")
active_runs: dict[tuple[str, str], bool] = {}
if run_id and not user_id:
raise ValueError("user_id is required when run_id is provided")
while True:
try:
if user_id and run_id:
runs = [(user_id, run_id)]
elif user_id:
runs = get_running_runs(user_id)
else:
runs = get_running_runs()
if not runs:
default_user = get_default_user_id()
if default_user:
runs = get_running_runs(default_user)
seen = set()
for scope_user, scope_run in runs:
if not scope_user or not scope_run:
continue
seen.add((scope_user, scope_run))
with engine_context(scope_user, scope_run):
cfg = load_strategy_config(user_id=scope_user, run_id=scope_run)
if not cfg.get("active"):
continue
strategy_name = cfg.get("strategy_name", "golden_nifty")
sip_amount = cfg.get("sip_amount", 0)
configured_frequency = cfg.get("sip_frequency") or {}
if not isinstance(configured_frequency, dict):
configured_frequency = {}
frequency_value = int(configured_frequency.get("value", cfg.get("frequency", 0)))
frequency_unit = configured_frequency.get("unit", cfg.get("unit", "days"))
frequency_info = {"value": frequency_value, "unit": frequency_unit}
frequency_label = f"{frequency_value} {frequency_unit}"
if not active_runs.get((scope_user, scope_run)):
log_event(
"ENGINE_START",
{
"strategy": strategy_name,
"sip_amount": sip_amount,
"frequency": frequency_label,
},
)
active_runs[(scope_user, scope_run)] = True
_update_engine_status(scope_user, scope_run, "RUNNING")
market_open, _ = india_market_status()
if not market_open:
log_event("MARKET_CLOSED", {"reason": "Outside market hours"})
continue
now = datetime.now()
mode = (cfg.get("mode") or "PAPER").strip().upper()
if mode not in {"PAPER", "LIVE"}:
mode = "PAPER"
state = load_state(mode=mode)
initial_cash = float(state.get("initial_cash") or 0.0)
broker = PaperBroker(initial_cash=initial_cash) if mode == "PAPER" else None
nifty_price = fetch_live_price(NIFTY)
gold_price = fetch_live_price(GOLD)
next_run = cfg.get("next_run")
if next_run is None or now >= datetime.fromisoformat(next_run):
nifty_hist = load_monthly_close(NIFTY)
gold_hist = load_monthly_close(GOLD)
nifty_sma = nifty_hist.rolling(SMA_MONTHS).mean().iloc[-1]
gold_sma = gold_hist.rolling(SMA_MONTHS).mean().iloc[-1]
eq_w, gd_w = allocation(
sp_price=nifty_price,
gd_price=gold_price,
sp_sma=nifty_sma,
gd_sma=gold_sma,
)
weights = {"equity": eq_w, "gold": gd_w}
state, executed = try_execute_sip(
now=now,
market_open=True,
sip_interval=frequency_to_timedelta(frequency_info).total_seconds(),
sip_amount=sip_amount,
sp_price=nifty_price,
gd_price=gold_price,
eq_w=eq_w,
gd_w=gd_w,
broker=broker,
mode=mode,
)
if executed:
log_event(
"SIP_TRIGGERED",
{
"date": now.date().isoformat(),
"allocation": weights,
"cash_used": sip_amount,
},
)
portfolio_value = (
state["nifty_units"] * nifty_price
+ state["gold_units"] * gold_price
)
log_event(
"PORTFOLIO_UPDATED",
{
"nifty_units": state["nifty_units"],
"gold_units": state["gold_units"],
"portfolio_value": portfolio_value,
},
)
cfg["next_run"] = (now + frequency_to_timedelta(frequency_info)).isoformat()
save_strategy_config(cfg, user_id=scope_user, run_id=scope_run)
if should_log_mtm(None, now):
state = load_state(mode=mode)
log_mtm(
nifty_units=state["nifty_units"],
gold_units=state["gold_units"],
nifty_price=nifty_price,
gold_price=gold_price,
total_invested=state["total_invested"],
logical_time=normalize_logical_time(now),
)
for key in list(active_runs.keys()):
if key not in seen:
active_runs.pop(key, None)
time.sleep(30)
except Exception as e:
log_event("ENGINE_ERROR", {"error": str(e)})
raise
if __name__ == "__main__":
run_engine()

View File

@ -1,157 +1,157 @@
# 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)
# 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)

View File

@ -1,34 +1,34 @@
# engine/history.py
import yfinance as yf
import pandas as pd
from pathlib import Path
ENGINE_ROOT = Path(__file__).resolve().parents[1]
STORAGE_DIR = ENGINE_ROOT / "storage"
STORAGE_DIR.mkdir(exist_ok=True)
CACHE_DIR = STORAGE_DIR / "history"
CACHE_DIR.mkdir(exist_ok=True)
def load_monthly_close(ticker, years=10):
file = CACHE_DIR / f"{ticker}.csv"
if file.exists():
df = pd.read_csv(file, parse_dates=["Date"], index_col="Date")
return df["Close"]
df = yf.download(
ticker,
period=f"{years}y",
auto_adjust=True,
progress=False,
timeout=5,
)
if df.empty:
raise RuntimeError(f"No history for {ticker}")
series = df["Close"].resample("M").last()
series.to_csv(file, header=["Close"])
return series
# engine/history.py
import yfinance as yf
import pandas as pd
from pathlib import Path
ENGINE_ROOT = Path(__file__).resolve().parents[1]
STORAGE_DIR = ENGINE_ROOT / "storage"
STORAGE_DIR.mkdir(exist_ok=True)
CACHE_DIR = STORAGE_DIR / "history"
CACHE_DIR.mkdir(exist_ok=True)
def load_monthly_close(ticker, years=10):
file = CACHE_DIR / f"{ticker}.csv"
if file.exists():
df = pd.read_csv(file, parse_dates=["Date"], index_col="Date")
return df["Close"]
df = yf.download(
ticker,
period=f"{years}y",
auto_adjust=True,
progress=False,
timeout=5,
)
if df.empty:
raise RuntimeError(f"No history for {ticker}")
series = df["Close"].resample("M").last()
series.to_csv(file, header=["Close"])
return series

View File

@ -1,113 +1,113 @@
# engine/ledger.py
from datetime import datetime, timezone
from indian_paper_trading_strategy.engine.db import insert_engine_event, run_with_retry, get_context
from indian_paper_trading_strategy.engine.time_utils import normalize_logical_time
def _event_exists_in_tx(cur, event, logical_time, 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)
cur.execute(
"""
SELECT 1
FROM event_ledger
WHERE user_id = %s AND run_id = %s AND event = %s AND logical_time = %s
LIMIT 1
""",
(scope_user, scope_run, event, logical_ts),
)
return cur.fetchone() is not None
def event_exists(event, logical_time, *, cur=None, user_id: str | None = None, run_id: str | None = None):
if cur is not None:
return _event_exists_in_tx(cur, event, logical_time, user_id=user_id, run_id=run_id)
def _op(cur, _conn):
return _event_exists_in_tx(cur, event, logical_time, user_id=user_id, run_id=run_id)
return run_with_retry(_op)
def _log_event_in_tx(
cur,
event,
payload,
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)
cur.execute(
"""
INSERT INTO event_ledger (
user_id,
run_id,
timestamp,
logical_time,
event,
nifty_units,
gold_units,
nifty_price,
gold_price,
amount
)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
ON CONFLICT DO NOTHING
""",
(
scope_user,
scope_run,
ts,
logical_ts,
event,
payload.get("nifty_units"),
payload.get("gold_units"),
payload.get("nifty_price"),
payload.get("gold_price"),
payload.get("amount"),
),
)
if cur.rowcount:
insert_engine_event(cur, event, data=payload, ts=ts)
def log_event(
event,
payload,
*,
cur=None,
ts=None,
logical_time=None,
user_id: str | None = None,
run_id: str | None = None,
):
now = ts or logical_time or datetime.utcnow().replace(tzinfo=timezone.utc)
if cur is not None:
_log_event_in_tx(
cur,
event,
payload,
now,
logical_time=logical_time,
user_id=user_id,
run_id=run_id,
)
return
def _op(cur, _conn):
_log_event_in_tx(
cur,
event,
payload,
now,
logical_time=logical_time,
user_id=user_id,
run_id=run_id,
)
return run_with_retry(_op)
# engine/ledger.py
from datetime import datetime, timezone
from indian_paper_trading_strategy.engine.db import insert_engine_event, run_with_retry, get_context
from indian_paper_trading_strategy.engine.time_utils import normalize_logical_time
def _event_exists_in_tx(cur, event, logical_time, 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)
cur.execute(
"""
SELECT 1
FROM event_ledger
WHERE user_id = %s AND run_id = %s AND event = %s AND logical_time = %s
LIMIT 1
""",
(scope_user, scope_run, event, logical_ts),
)
return cur.fetchone() is not None
def event_exists(event, logical_time, *, cur=None, user_id: str | None = None, run_id: str | None = None):
if cur is not None:
return _event_exists_in_tx(cur, event, logical_time, user_id=user_id, run_id=run_id)
def _op(cur, _conn):
return _event_exists_in_tx(cur, event, logical_time, user_id=user_id, run_id=run_id)
return run_with_retry(_op)
def _log_event_in_tx(
cur,
event,
payload,
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)
cur.execute(
"""
INSERT INTO event_ledger (
user_id,
run_id,
timestamp,
logical_time,
event,
nifty_units,
gold_units,
nifty_price,
gold_price,
amount
)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
ON CONFLICT DO NOTHING
""",
(
scope_user,
scope_run,
ts,
logical_ts,
event,
payload.get("nifty_units"),
payload.get("gold_units"),
payload.get("nifty_price"),
payload.get("gold_price"),
payload.get("amount"),
),
)
if cur.rowcount:
insert_engine_event(cur, event, data=payload, ts=ts)
def log_event(
event,
payload,
*,
cur=None,
ts=None,
logical_time=None,
user_id: str | None = None,
run_id: str | None = None,
):
now = ts or logical_time or datetime.utcnow().replace(tzinfo=timezone.utc)
if cur is not None:
_log_event_in_tx(
cur,
event,
payload,
now,
logical_time=logical_time,
user_id=user_id,
run_id=run_id,
)
return
def _op(cur, _conn):
_log_event_in_tx(
cur,
event,
payload,
now,
logical_time=logical_time,
user_id=user_id,
run_id=run_id,
)
return run_with_retry(_op)

View File

@ -1,42 +1,42 @@
# engine/market.py
from datetime import datetime, time as dtime, timedelta
import pytz
_MARKET_TZ = pytz.timezone("Asia/Kolkata")
_OPEN_T = dtime(9, 15)
_CLOSE_T = dtime(15, 30)
def _as_market_tz(value: datetime) -> datetime:
if value.tzinfo is None:
return _MARKET_TZ.localize(value)
return value.astimezone(_MARKET_TZ)
def is_market_open(now: datetime) -> bool:
now = _as_market_tz(now)
return now.weekday() < 5 and _OPEN_T <= now.time() <= _CLOSE_T
def india_market_status():
now = datetime.now(_MARKET_TZ)
return is_market_open(now), now
def next_market_open_after(value: datetime) -> datetime:
current = _as_market_tz(value)
while current.weekday() >= 5:
current = current + timedelta(days=1)
current = current.replace(hour=_OPEN_T.hour, minute=_OPEN_T.minute, second=0, microsecond=0)
if current.time() < _OPEN_T:
return current.replace(hour=_OPEN_T.hour, minute=_OPEN_T.minute, second=0, microsecond=0)
if current.time() > _CLOSE_T:
current = current + timedelta(days=1)
while current.weekday() >= 5:
current = current + timedelta(days=1)
return current.replace(hour=_OPEN_T.hour, minute=_OPEN_T.minute, second=0, microsecond=0)
return current
def align_to_market_open(value: datetime) -> datetime:
current = _as_market_tz(value)
aligned = current if is_market_open(current) else next_market_open_after(current)
if value.tzinfo is None:
return aligned.replace(tzinfo=None)
return aligned
# engine/market.py
from datetime import datetime, time as dtime, timedelta
import pytz
_MARKET_TZ = pytz.timezone("Asia/Kolkata")
_OPEN_T = dtime(9, 15)
_CLOSE_T = dtime(15, 30)
def _as_market_tz(value: datetime) -> datetime:
if value.tzinfo is None:
return _MARKET_TZ.localize(value)
return value.astimezone(_MARKET_TZ)
def is_market_open(now: datetime) -> bool:
now = _as_market_tz(now)
return now.weekday() < 5 and _OPEN_T <= now.time() <= _CLOSE_T
def india_market_status():
now = datetime.now(_MARKET_TZ)
return is_market_open(now), now
def next_market_open_after(value: datetime) -> datetime:
current = _as_market_tz(value)
while current.weekday() >= 5:
current = current + timedelta(days=1)
current = current.replace(hour=_OPEN_T.hour, minute=_OPEN_T.minute, second=0, microsecond=0)
if current.time() < _OPEN_T:
return current.replace(hour=_OPEN_T.hour, minute=_OPEN_T.minute, second=0, microsecond=0)
if current.time() > _CLOSE_T:
current = current + timedelta(days=1)
while current.weekday() >= 5:
current = current + timedelta(days=1)
return current.replace(hour=_OPEN_T.hour, minute=_OPEN_T.minute, second=0, microsecond=0)
return current
def align_to_market_open(value: datetime) -> datetime:
current = _as_market_tz(value)
aligned = current if is_market_open(current) else next_market_open_after(current)
if value.tzinfo is None:
return aligned.replace(tzinfo=None)
return aligned

View File

@ -1,154 +1,154 @@
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
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

View File

@ -1,58 +1,58 @@
import os
import threading
import time
from datetime import datetime, timedelta, timezone
from indian_paper_trading_strategy.engine.market import is_market_open, align_to_market_open
from indian_paper_trading_strategy.engine.execution import try_execute_sip
from indian_paper_trading_strategy.engine.broker import PaperBroker
from indian_paper_trading_strategy.engine.mtm import log_mtm, should_log_mtm
from indian_paper_trading_strategy.engine.state import load_state
from indian_paper_trading_strategy.engine.data import fetch_live_price
from indian_paper_trading_strategy.engine.history import load_monthly_close
from indian_paper_trading_strategy.engine.strategy import allocation
from indian_paper_trading_strategy.engine.time_utils import normalize_logical_time
from indian_paper_trading_strategy.engine.db import db_transaction, insert_engine_event, run_with_retry, get_context, set_context
def _update_engine_status(user_id: str, run_id: str, status: str):
now = datetime.utcnow().replace(tzinfo=timezone.utc)
def _op(cur, _conn):
cur.execute(
"""
INSERT INTO engine_status (user_id, run_id, status, last_updated)
VALUES (%s, %s, %s, %s)
ON CONFLICT (user_id, run_id) DO UPDATE
SET status = EXCLUDED.status,
last_updated = EXCLUDED.last_updated
""",
(user_id, run_id, status, now),
)
run_with_retry(_op)
NIFTY = "NIFTYBEES.NS"
GOLD = "GOLDBEES.NS"
SMA_MONTHS = 36
_DEFAULT_ENGINE_STATE = {
"state": "STOPPED",
"run_id": None,
"user_id": None,
"last_heartbeat_ts": None,
}
import os
import threading
import time
from datetime import datetime, timedelta, timezone
from indian_paper_trading_strategy.engine.market import is_market_open, align_to_market_open
from indian_paper_trading_strategy.engine.execution import try_execute_sip
from indian_paper_trading_strategy.engine.broker import PaperBroker
from indian_paper_trading_strategy.engine.mtm import log_mtm, should_log_mtm
from indian_paper_trading_strategy.engine.state import load_state
from indian_paper_trading_strategy.engine.data import fetch_live_price
from indian_paper_trading_strategy.engine.history import load_monthly_close
from indian_paper_trading_strategy.engine.strategy import allocation
from indian_paper_trading_strategy.engine.time_utils import normalize_logical_time
from indian_paper_trading_strategy.engine.db import db_transaction, insert_engine_event, run_with_retry, get_context, set_context
def _update_engine_status(user_id: str, run_id: str, status: str):
now = datetime.utcnow().replace(tzinfo=timezone.utc)
def _op(cur, _conn):
cur.execute(
"""
INSERT INTO engine_status (user_id, run_id, status, last_updated)
VALUES (%s, %s, %s, %s)
ON CONFLICT (user_id, run_id) DO UPDATE
SET status = EXCLUDED.status,
last_updated = EXCLUDED.last_updated
""",
(user_id, run_id, status, now),
)
run_with_retry(_op)
NIFTY = "NIFTYBEES.NS"
GOLD = "GOLDBEES.NS"
SMA_MONTHS = 36
_DEFAULT_ENGINE_STATE = {
"state": "STOPPED",
"run_id": None,
"user_id": None,
"last_heartbeat_ts": None,
}
_ENGINE_STATES = {}
_ENGINE_STATES_LOCK = threading.Lock()
_RUNNERS = {}
_RUNNERS_LOCK = threading.Lock()
engine_state = _ENGINE_STATES
engine_state = _ENGINE_STATES
def _state_key(user_id: str, run_id: str):
return (user_id, run_id)
@ -84,38 +84,38 @@ def _set_state(user_id: str, run_id: str, **updates):
def get_engine_state(user_id: str, run_id: str):
state = _get_state(user_id, run_id)
return dict(state)
def log_event(
event: str,
data: dict | None = None,
message: str | None = None,
meta: dict | None = None,
):
entry = {
"ts": datetime.utcnow().replace(tzinfo=timezone.utc).isoformat(),
"event": event,
}
if message is not None or meta is not None:
entry["message"] = message or ""
entry["meta"] = meta or {}
else:
entry["data"] = data or {}
event_ts = datetime.fromisoformat(entry["ts"].replace("Z", "+00:00"))
data = entry.get("data") if "data" in entry else None
meta = entry.get("meta") if "meta" in entry else None
def _op(cur, _conn):
insert_engine_event(
cur,
entry.get("event"),
data=data,
message=entry.get("message"),
meta=meta,
ts=event_ts,
)
run_with_retry(_op)
def log_event(
event: str,
data: dict | None = None,
message: str | None = None,
meta: dict | None = None,
):
entry = {
"ts": datetime.utcnow().replace(tzinfo=timezone.utc).isoformat(),
"event": event,
}
if message is not None or meta is not None:
entry["message"] = message or ""
entry["meta"] = meta or {}
else:
entry["data"] = data or {}
event_ts = datetime.fromisoformat(entry["ts"].replace("Z", "+00:00"))
data = entry.get("data") if "data" in entry else None
meta = entry.get("meta") if "meta" in entry else None
def _op(cur, _conn):
insert_engine_event(
cur,
entry.get("event"),
data=data,
message=entry.get("message"),
meta=meta,
ts=event_ts,
)
run_with_retry(_op)
def sleep_with_heartbeat(
total_seconds: int,
stop_event: threading.Event,
@ -133,321 +133,321 @@ def _clear_runner(user_id: str, run_id: str):
key = _state_key(user_id, run_id)
with _RUNNERS_LOCK:
_RUNNERS.pop(key, None)
def can_execute(now: datetime) -> tuple[bool, str]:
if not is_market_open(now):
return False, "MARKET_CLOSED"
return True, "OK"
def _engine_loop(config, stop_event: threading.Event):
print("Strategy engine started with config:", config)
user_id = config.get("user_id")
run_id = config.get("run_id")
scope_user, scope_run = get_context(user_id, run_id)
set_context(scope_user, scope_run)
strategy_name = config.get("strategy_name") or config.get("strategy") or "golden_nifty"
sip_amount = config["sip_amount"]
configured_frequency = config.get("sip_frequency") or {}
if not isinstance(configured_frequency, dict):
configured_frequency = {}
frequency_value = int(configured_frequency.get("value", config.get("frequency", 0)))
frequency_unit = configured_frequency.get("unit", config.get("unit", "days"))
frequency_label = f"{frequency_value} {frequency_unit}"
emit_event_cb = config.get("emit_event")
if not callable(emit_event_cb):
emit_event_cb = None
debug_enabled = os.getenv("ENGINE_DEBUG", "1").strip().lower() not in {"0", "false", "no"}
def debug_event(event: str, message: str, meta: dict | None = None):
if not debug_enabled:
return
try:
log_event(event=event, message=message, meta=meta or {})
except Exception:
pass
if emit_event_cb:
emit_event_cb(event=event, message=message, meta=meta or {})
print(f"[ENGINE] {event} {message} {meta or {}}", flush=True)
mode = (config.get("mode") or "LIVE").strip().upper()
if mode not in {"PAPER", "LIVE"}:
mode = "LIVE"
broker_type = config.get("broker") or "paper"
if broker_type != "paper":
broker_type = "paper"
if broker_type == "paper":
mode = "PAPER"
initial_cash = float(config.get("initial_cash", 0))
broker = PaperBroker(initial_cash=initial_cash)
log_event(
event="DEBUG_PAPER_STORE_PATH",
message="Paper broker store path",
meta={
"cwd": os.getcwd(),
"paper_store_path": str(broker.store_path) if hasattr(broker, "store_path") else "NO_STORE_PATH",
"abs_store_path": os.path.abspath(str(broker.store_path)) if hasattr(broker, "store_path") else "N/A",
},
)
if emit_event_cb:
emit_event_cb(
event="DEBUG_PAPER_STORE_PATH",
message="Paper broker store path",
meta={
"cwd": os.getcwd(),
"paper_store_path": str(broker.store_path) if hasattr(broker, "store_path") else "NO_STORE_PATH",
"abs_store_path": os.path.abspath(str(broker.store_path)) if hasattr(broker, "store_path") else "N/A",
},
)
log_event("ENGINE_START", {
"strategy": strategy_name,
"sip_amount": sip_amount,
"frequency": frequency_label,
})
debug_event("ENGINE_START_DEBUG", "engine loop started", {"run_id": scope_run, "user_id": scope_user})
def can_execute(now: datetime) -> tuple[bool, str]:
if not is_market_open(now):
return False, "MARKET_CLOSED"
return True, "OK"
def _engine_loop(config, stop_event: threading.Event):
print("Strategy engine started with config:", config)
user_id = config.get("user_id")
run_id = config.get("run_id")
scope_user, scope_run = get_context(user_id, run_id)
set_context(scope_user, scope_run)
strategy_name = config.get("strategy_name") or config.get("strategy") or "golden_nifty"
sip_amount = config["sip_amount"]
configured_frequency = config.get("sip_frequency") or {}
if not isinstance(configured_frequency, dict):
configured_frequency = {}
frequency_value = int(configured_frequency.get("value", config.get("frequency", 0)))
frequency_unit = configured_frequency.get("unit", config.get("unit", "days"))
frequency_label = f"{frequency_value} {frequency_unit}"
emit_event_cb = config.get("emit_event")
if not callable(emit_event_cb):
emit_event_cb = None
debug_enabled = os.getenv("ENGINE_DEBUG", "1").strip().lower() not in {"0", "false", "no"}
def debug_event(event: str, message: str, meta: dict | None = None):
if not debug_enabled:
return
try:
log_event(event=event, message=message, meta=meta or {})
except Exception:
pass
if emit_event_cb:
emit_event_cb(event=event, message=message, meta=meta or {})
print(f"[ENGINE] {event} {message} {meta or {}}", flush=True)
mode = (config.get("mode") or "LIVE").strip().upper()
if mode not in {"PAPER", "LIVE"}:
mode = "LIVE"
broker_type = config.get("broker") or "paper"
if broker_type != "paper":
broker_type = "paper"
if broker_type == "paper":
mode = "PAPER"
initial_cash = float(config.get("initial_cash", 0))
broker = PaperBroker(initial_cash=initial_cash)
log_event(
event="DEBUG_PAPER_STORE_PATH",
message="Paper broker store path",
meta={
"cwd": os.getcwd(),
"paper_store_path": str(broker.store_path) if hasattr(broker, "store_path") else "NO_STORE_PATH",
"abs_store_path": os.path.abspath(str(broker.store_path)) if hasattr(broker, "store_path") else "N/A",
},
)
if emit_event_cb:
emit_event_cb(
event="DEBUG_PAPER_STORE_PATH",
message="Paper broker store path",
meta={
"cwd": os.getcwd(),
"paper_store_path": str(broker.store_path) if hasattr(broker, "store_path") else "NO_STORE_PATH",
"abs_store_path": os.path.abspath(str(broker.store_path)) if hasattr(broker, "store_path") else "N/A",
},
)
log_event("ENGINE_START", {
"strategy": strategy_name,
"sip_amount": sip_amount,
"frequency": frequency_label,
})
debug_event("ENGINE_START_DEBUG", "engine loop started", {"run_id": scope_run, "user_id": scope_user})
_set_state(
scope_user,
scope_run,
state="RUNNING",
last_heartbeat_ts=datetime.utcnow().isoformat() + "Z",
)
_update_engine_status(scope_user, scope_run, "RUNNING")
try:
while not stop_event.is_set():
_update_engine_status(scope_user, scope_run, "RUNNING")
try:
while not stop_event.is_set():
_set_state(scope_user, scope_run, last_heartbeat_ts=datetime.utcnow().isoformat() + "Z")
_update_engine_status(scope_user, scope_run, "RUNNING")
state = load_state(mode=mode)
debug_event(
"STATE_LOADED",
"loaded engine state",
{
"last_sip_ts": state.get("last_sip_ts"),
"last_run": state.get("last_run"),
"cash": state.get("cash"),
"total_invested": state.get("total_invested"),
},
)
state_frequency = state.get("sip_frequency")
if not isinstance(state_frequency, dict):
state_frequency = {"value": frequency_value, "unit": frequency_unit}
freq = int(state_frequency.get("value", frequency_value))
unit = state_frequency.get("unit", frequency_unit)
frequency_label = f"{freq} {unit}"
if unit == "minutes":
delta = timedelta(minutes=freq)
else:
delta = timedelta(days=freq)
# Gate 2: time to SIP
last_run = state.get("last_run") or state.get("last_sip_ts")
is_first_run = last_run is None
now = datetime.now()
debug_event(
"ENGINE_LOOP_TICK",
"engine loop tick",
{"now": now.isoformat(), "frequency": frequency_label},
)
if last_run and not is_first_run:
next_run = datetime.fromisoformat(last_run) + delta
next_run = align_to_market_open(next_run)
if now < next_run:
log_event(
event="SIP_WAITING",
message="Waiting for next SIP window",
meta={
"last_run": last_run,
"next_eligible": next_run.isoformat(),
"now": now.isoformat(),
"frequency": frequency_label,
},
)
if emit_event_cb:
emit_event_cb(
event="SIP_WAITING",
message="Waiting for next SIP window",
meta={
"last_run": last_run,
"next_eligible": next_run.isoformat(),
"now": now.isoformat(),
"frequency": frequency_label,
},
)
state = load_state(mode=mode)
debug_event(
"STATE_LOADED",
"loaded engine state",
{
"last_sip_ts": state.get("last_sip_ts"),
"last_run": state.get("last_run"),
"cash": state.get("cash"),
"total_invested": state.get("total_invested"),
},
)
state_frequency = state.get("sip_frequency")
if not isinstance(state_frequency, dict):
state_frequency = {"value": frequency_value, "unit": frequency_unit}
freq = int(state_frequency.get("value", frequency_value))
unit = state_frequency.get("unit", frequency_unit)
frequency_label = f"{freq} {unit}"
if unit == "minutes":
delta = timedelta(minutes=freq)
else:
delta = timedelta(days=freq)
# Gate 2: time to SIP
last_run = state.get("last_run") or state.get("last_sip_ts")
is_first_run = last_run is None
now = datetime.now()
debug_event(
"ENGINE_LOOP_TICK",
"engine loop tick",
{"now": now.isoformat(), "frequency": frequency_label},
)
if last_run and not is_first_run:
next_run = datetime.fromisoformat(last_run) + delta
next_run = align_to_market_open(next_run)
if now < next_run:
log_event(
event="SIP_WAITING",
message="Waiting for next SIP window",
meta={
"last_run": last_run,
"next_eligible": next_run.isoformat(),
"now": now.isoformat(),
"frequency": frequency_label,
},
)
if emit_event_cb:
emit_event_cb(
event="SIP_WAITING",
message="Waiting for next SIP window",
meta={
"last_run": last_run,
"next_eligible": next_run.isoformat(),
"now": now.isoformat(),
"frequency": frequency_label,
},
)
sleep_with_heartbeat(60, stop_event, scope_user, scope_run)
continue
try:
debug_event("PRICE_FETCH_START", "fetching live prices", {"tickers": [NIFTY, GOLD]})
nifty_price = fetch_live_price(NIFTY)
gold_price = fetch_live_price(GOLD)
debug_event(
"PRICE_FETCHED",
"fetched live prices",
{"nifty_price": float(nifty_price), "gold_price": float(gold_price)},
)
except Exception as exc:
debug_event("PRICE_FETCH_ERROR", "live price fetch failed", {"error": str(exc)})
try:
debug_event("PRICE_FETCH_START", "fetching live prices", {"tickers": [NIFTY, GOLD]})
nifty_price = fetch_live_price(NIFTY)
gold_price = fetch_live_price(GOLD)
debug_event(
"PRICE_FETCHED",
"fetched live prices",
{"nifty_price": float(nifty_price), "gold_price": float(gold_price)},
)
except Exception as exc:
debug_event("PRICE_FETCH_ERROR", "live price fetch failed", {"error": str(exc)})
sleep_with_heartbeat(30, stop_event, scope_user, scope_run)
continue
try:
nifty_hist = load_monthly_close(NIFTY)
gold_hist = load_monthly_close(GOLD)
except Exception as exc:
debug_event("HISTORY_LOAD_ERROR", "history load failed", {"error": str(exc)})
try:
nifty_hist = load_monthly_close(NIFTY)
gold_hist = load_monthly_close(GOLD)
except Exception as exc:
debug_event("HISTORY_LOAD_ERROR", "history load failed", {"error": str(exc)})
sleep_with_heartbeat(30, stop_event, scope_user, scope_run)
continue
nifty_sma = nifty_hist.rolling(SMA_MONTHS).mean().iloc[-1]
gold_sma = gold_hist.rolling(SMA_MONTHS).mean().iloc[-1]
eq_w, gd_w = allocation(
sp_price=nifty_price,
gd_price=gold_price,
sp_sma=nifty_sma,
gd_sma=gold_sma
)
debug_event(
"WEIGHTS_COMPUTED",
"computed allocation weights",
{"equity_weight": float(eq_w), "gold_weight": float(gd_w)},
)
weights = {"equity": eq_w, "gold": gd_w}
allowed, reason = can_execute(now)
executed = False
if not allowed:
log_event(
event="EXECUTION_BLOCKED",
message="Execution blocked by market gate",
meta={
"reason": reason,
"eligible_since": last_run,
"checked_at": now.isoformat(),
},
)
debug_event("MARKET_GATE", "market closed", {"reason": reason})
if emit_event_cb:
emit_event_cb(
event="EXECUTION_BLOCKED",
message="Execution blocked by market gate",
meta={
"reason": reason,
"eligible_since": last_run,
"checked_at": now.isoformat(),
},
)
else:
log_event(
event="DEBUG_BEFORE_TRY_EXECUTE",
message="About to call try_execute_sip",
meta={
"last_run": last_run,
"frequency": frequency_label,
"allowed": allowed,
"reason": reason,
"sip_amount": sip_amount,
"broker": type(broker).__name__,
"now": now.isoformat(),
},
)
if emit_event_cb:
emit_event_cb(
event="DEBUG_BEFORE_TRY_EXECUTE",
message="About to call try_execute_sip",
meta={
"last_run": last_run,
"frequency": frequency_label,
"allowed": allowed,
"reason": reason,
"sip_amount": sip_amount,
"broker": type(broker).__name__,
"now": now.isoformat(),
},
)
debug_event(
"TRY_EXECUTE_START",
"calling try_execute_sip",
{"sip_interval_sec": delta.total_seconds(), "sip_amount": sip_amount},
)
state, executed = try_execute_sip(
now=now,
market_open=True,
sip_interval=delta.total_seconds(),
sip_amount=sip_amount,
sp_price=nifty_price,
gd_price=gold_price,
eq_w=eq_w,
gd_w=gd_w,
broker=broker,
mode=mode,
)
log_event(
event="DEBUG_AFTER_TRY_EXECUTE",
message="Returned from try_execute_sip",
meta={
"executed": executed,
"state_last_run": state.get("last_run"),
"state_last_sip_ts": state.get("last_sip_ts"),
},
)
if emit_event_cb:
emit_event_cb(
event="DEBUG_AFTER_TRY_EXECUTE",
message="Returned from try_execute_sip",
meta={
"executed": executed,
"state_last_run": state.get("last_run"),
"state_last_sip_ts": state.get("last_sip_ts"),
},
)
debug_event(
"TRY_EXECUTE_DONE",
"try_execute_sip finished",
{"executed": executed, "last_run": state.get("last_run")},
)
if executed:
log_event("SIP_TRIGGERED", {
"date": now.date().isoformat(),
"allocation": weights,
"cash_used": sip_amount
})
debug_event("SIP_TRIGGERED", "sip executed", {"cash_used": sip_amount})
portfolio_value = (
state["nifty_units"] * nifty_price
+ state["gold_units"] * gold_price
)
log_event("PORTFOLIO_UPDATED", {
"nifty_units": state["nifty_units"],
"gold_units": state["gold_units"],
"portfolio_value": portfolio_value
})
print("SIP executed at", now)
if should_log_mtm(None, now):
logical_time = normalize_logical_time(now)
with db_transaction() as cur:
log_mtm(
nifty_units=state["nifty_units"],
gold_units=state["gold_units"],
nifty_price=nifty_price,
gold_price=gold_price,
total_invested=state["total_invested"],
cur=cur,
logical_time=logical_time,
)
broker.update_equity(
{NIFTY: nifty_price, GOLD: gold_price},
now,
cur=cur,
logical_time=logical_time,
)
nifty_sma = nifty_hist.rolling(SMA_MONTHS).mean().iloc[-1]
gold_sma = gold_hist.rolling(SMA_MONTHS).mean().iloc[-1]
eq_w, gd_w = allocation(
sp_price=nifty_price,
gd_price=gold_price,
sp_sma=nifty_sma,
gd_sma=gold_sma
)
debug_event(
"WEIGHTS_COMPUTED",
"computed allocation weights",
{"equity_weight": float(eq_w), "gold_weight": float(gd_w)},
)
weights = {"equity": eq_w, "gold": gd_w}
allowed, reason = can_execute(now)
executed = False
if not allowed:
log_event(
event="EXECUTION_BLOCKED",
message="Execution blocked by market gate",
meta={
"reason": reason,
"eligible_since": last_run,
"checked_at": now.isoformat(),
},
)
debug_event("MARKET_GATE", "market closed", {"reason": reason})
if emit_event_cb:
emit_event_cb(
event="EXECUTION_BLOCKED",
message="Execution blocked by market gate",
meta={
"reason": reason,
"eligible_since": last_run,
"checked_at": now.isoformat(),
},
)
else:
log_event(
event="DEBUG_BEFORE_TRY_EXECUTE",
message="About to call try_execute_sip",
meta={
"last_run": last_run,
"frequency": frequency_label,
"allowed": allowed,
"reason": reason,
"sip_amount": sip_amount,
"broker": type(broker).__name__,
"now": now.isoformat(),
},
)
if emit_event_cb:
emit_event_cb(
event="DEBUG_BEFORE_TRY_EXECUTE",
message="About to call try_execute_sip",
meta={
"last_run": last_run,
"frequency": frequency_label,
"allowed": allowed,
"reason": reason,
"sip_amount": sip_amount,
"broker": type(broker).__name__,
"now": now.isoformat(),
},
)
debug_event(
"TRY_EXECUTE_START",
"calling try_execute_sip",
{"sip_interval_sec": delta.total_seconds(), "sip_amount": sip_amount},
)
state, executed = try_execute_sip(
now=now,
market_open=True,
sip_interval=delta.total_seconds(),
sip_amount=sip_amount,
sp_price=nifty_price,
gd_price=gold_price,
eq_w=eq_w,
gd_w=gd_w,
broker=broker,
mode=mode,
)
log_event(
event="DEBUG_AFTER_TRY_EXECUTE",
message="Returned from try_execute_sip",
meta={
"executed": executed,
"state_last_run": state.get("last_run"),
"state_last_sip_ts": state.get("last_sip_ts"),
},
)
if emit_event_cb:
emit_event_cb(
event="DEBUG_AFTER_TRY_EXECUTE",
message="Returned from try_execute_sip",
meta={
"executed": executed,
"state_last_run": state.get("last_run"),
"state_last_sip_ts": state.get("last_sip_ts"),
},
)
debug_event(
"TRY_EXECUTE_DONE",
"try_execute_sip finished",
{"executed": executed, "last_run": state.get("last_run")},
)
if executed:
log_event("SIP_TRIGGERED", {
"date": now.date().isoformat(),
"allocation": weights,
"cash_used": sip_amount
})
debug_event("SIP_TRIGGERED", "sip executed", {"cash_used": sip_amount})
portfolio_value = (
state["nifty_units"] * nifty_price
+ state["gold_units"] * gold_price
)
log_event("PORTFOLIO_UPDATED", {
"nifty_units": state["nifty_units"],
"gold_units": state["gold_units"],
"portfolio_value": portfolio_value
})
print("SIP executed at", now)
if should_log_mtm(None, now):
logical_time = normalize_logical_time(now)
with db_transaction() as cur:
log_mtm(
nifty_units=state["nifty_units"],
gold_units=state["gold_units"],
nifty_price=nifty_price,
gold_price=gold_price,
total_invested=state["total_invested"],
cur=cur,
logical_time=logical_time,
)
broker.update_equity(
{NIFTY: nifty_price, GOLD: gold_price},
now,
cur=cur,
logical_time=logical_time,
)
sleep_with_heartbeat(30, stop_event, scope_user, scope_run)
except Exception as e:
_set_state(scope_user, scope_run, state="ERROR", last_heartbeat_ts=datetime.utcnow().isoformat() + "Z")
@ -465,7 +465,7 @@ def _engine_loop(config, stop_event: threading.Event):
_update_engine_status(scope_user, scope_run, "STOPPED")
print("Strategy engine stopped")
_clear_runner(scope_user, scope_run)
def start_engine(config):
user_id = config.get("user_id")
run_id = config.get("run_id")
@ -481,8 +481,8 @@ def start_engine(config):
return False
stop_event = threading.Event()
thread = threading.Thread(
target=_engine_loop,
thread = threading.Thread(
target=_engine_loop,
args=(config, stop_event),
daemon=True,
)
@ -515,4 +515,4 @@ def stop_engine(user_id: str, run_id: str | None = None, timeout: float | None =
else:
stopped_all = False
return stopped_all

View File

@ -1,303 +1,303 @@
# engine/state.py
from datetime import datetime, timezone
from indian_paper_trading_strategy.engine.db import db_connection, insert_engine_event, run_with_retry, get_context
DEFAULT_STATE = {
"initial_cash": 0.0,
"cash": 0.0,
"total_invested": 0.0,
"nifty_units": 0.0,
"gold_units": 0.0,
"last_sip_ts": None,
"last_run": None,
"sip_frequency": None,
}
DEFAULT_PAPER_STATE = {
**DEFAULT_STATE,
"initial_cash": 1_000_000.0,
"cash": 1_000_000.0,
"sip_frequency": {"value": 30, "unit": "days"},
}
def _state_key(mode: str | None):
key = (mode or "LIVE").strip().upper()
return "PAPER" if key == "PAPER" else "LIVE"
def _default_state(mode: str | None):
if _state_key(mode) == "PAPER":
return DEFAULT_PAPER_STATE.copy()
return DEFAULT_STATE.copy()
def _local_tz():
return datetime.now().astimezone().tzinfo
def _format_local_ts(value: datetime | None):
if value is None:
return None
return value.astimezone(_local_tz()).replace(tzinfo=None).isoformat()
def _parse_ts(value):
if value is None:
return None
if isinstance(value, datetime):
if value.tzinfo is None:
return value.replace(tzinfo=_local_tz())
return value
if isinstance(value, str):
text = value.strip()
if not text:
return None
try:
parsed = datetime.fromisoformat(text.replace("Z", "+00:00"))
except ValueError:
return None
if parsed.tzinfo is None:
parsed = parsed.replace(tzinfo=_local_tz())
return parsed
return None
def _resolve_scope(user_id: str | None, run_id: str | None):
return get_context(user_id, run_id)
def load_state(
mode: str | None = "LIVE",
*,
cur=None,
for_update: bool = False,
user_id: str | None = None,
run_id: str | None = None,
):
scope_user, scope_run = _resolve_scope(user_id, run_id)
key = _state_key(mode)
if key == "PAPER":
if cur is None:
with db_connection() as conn:
with conn.cursor() as cur:
return load_state(
mode=mode,
cur=cur,
for_update=for_update,
user_id=scope_user,
run_id=scope_run,
)
lock_clause = " FOR UPDATE" if for_update else ""
cur.execute(
f"""
SELECT initial_cash, cash, total_invested, nifty_units, gold_units,
last_sip_ts, last_run, sip_frequency_value, sip_frequency_unit
FROM engine_state_paper
WHERE user_id = %s AND run_id = %s{lock_clause}
LIMIT 1
""",
(scope_user, scope_run),
)
row = cur.fetchone()
if not row:
return _default_state(mode)
merged = _default_state(mode)
merged.update(
{
"initial_cash": float(row[0]) if row[0] is not None else merged["initial_cash"],
"cash": float(row[1]) if row[1] is not None else merged["cash"],
"total_invested": float(row[2]) if row[2] is not None else merged["total_invested"],
"nifty_units": float(row[3]) if row[3] is not None else merged["nifty_units"],
"gold_units": float(row[4]) if row[4] is not None else merged["gold_units"],
"last_sip_ts": _format_local_ts(row[5]),
"last_run": _format_local_ts(row[6]),
}
)
if row[7] is not None or row[8] is not None:
merged["sip_frequency"] = {"value": row[7], "unit": row[8]}
return merged
if cur is None:
with db_connection() as conn:
with conn.cursor() as cur:
return load_state(
mode=mode,
cur=cur,
for_update=for_update,
user_id=scope_user,
run_id=scope_run,
)
lock_clause = " FOR UPDATE" if for_update else ""
cur.execute(
f"""
SELECT total_invested, nifty_units, gold_units, last_sip_ts, last_run
FROM engine_state
WHERE user_id = %s AND run_id = %s{lock_clause}
LIMIT 1
""",
(scope_user, scope_run),
)
row = cur.fetchone()
if not row:
return _default_state(mode)
merged = _default_state(mode)
merged.update(
{
"total_invested": float(row[0]) if row[0] is not None else merged["total_invested"],
"nifty_units": float(row[1]) if row[1] is not None else merged["nifty_units"],
"gold_units": float(row[2]) if row[2] is not None else merged["gold_units"],
"last_sip_ts": _format_local_ts(row[3]),
"last_run": _format_local_ts(row[4]),
}
)
return merged
def init_paper_state(
initial_cash: float,
sip_frequency: dict | None = None,
*,
cur=None,
user_id: str | None = None,
run_id: str | None = None,
):
state = DEFAULT_PAPER_STATE.copy()
state.update(
{
"initial_cash": float(initial_cash),
"cash": float(initial_cash),
"total_invested": 0.0,
"nifty_units": 0.0,
"gold_units": 0.0,
"last_sip_ts": None,
"last_run": None,
"sip_frequency": sip_frequency or state.get("sip_frequency"),
}
)
save_state(state, mode="PAPER", cur=cur, emit_event=True, user_id=user_id, run_id=run_id)
return state
def save_state(
state,
mode: str | None = "LIVE",
*,
cur=None,
emit_event: bool = False,
event_meta: dict | None = None,
user_id: str | None = None,
run_id: str | None = None,
):
scope_user, scope_run = _resolve_scope(user_id, run_id)
key = _state_key(mode)
last_sip_ts = _parse_ts(state.get("last_sip_ts"))
last_run = _parse_ts(state.get("last_run"))
if key == "PAPER":
sip_frequency = state.get("sip_frequency")
sip_value = None
sip_unit = None
if isinstance(sip_frequency, dict):
sip_value = sip_frequency.get("value")
sip_unit = sip_frequency.get("unit")
def _save(cur):
cur.execute(
"""
INSERT INTO engine_state_paper (
user_id, run_id, initial_cash, cash, total_invested, nifty_units, gold_units,
last_sip_ts, last_run, sip_frequency_value, sip_frequency_unit
)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (user_id, run_id) DO UPDATE
SET initial_cash = EXCLUDED.initial_cash,
cash = EXCLUDED.cash,
total_invested = EXCLUDED.total_invested,
nifty_units = EXCLUDED.nifty_units,
gold_units = EXCLUDED.gold_units,
last_sip_ts = EXCLUDED.last_sip_ts,
last_run = EXCLUDED.last_run,
sip_frequency_value = EXCLUDED.sip_frequency_value,
sip_frequency_unit = EXCLUDED.sip_frequency_unit
""",
(
scope_user,
scope_run,
float(state.get("initial_cash", 0.0)),
float(state.get("cash", 0.0)),
float(state.get("total_invested", 0.0)),
float(state.get("nifty_units", 0.0)),
float(state.get("gold_units", 0.0)),
last_sip_ts,
last_run,
sip_value,
sip_unit,
),
)
if emit_event:
insert_engine_event(
cur,
"STATE_UPDATED",
data={
"mode": "PAPER",
"cash": state.get("cash"),
"total_invested": state.get("total_invested"),
"nifty_units": state.get("nifty_units"),
"gold_units": state.get("gold_units"),
"last_sip_ts": state.get("last_sip_ts"),
"last_run": state.get("last_run"),
},
meta=event_meta,
ts=datetime.utcnow().replace(tzinfo=timezone.utc),
)
if cur is not None:
_save(cur)
return
def _op(cur, _conn):
_save(cur)
return run_with_retry(_op)
def _save(cur):
cur.execute(
"""
INSERT INTO engine_state (
user_id, run_id, total_invested, nifty_units, gold_units, last_sip_ts, last_run
)
VALUES (%s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (user_id, run_id) DO UPDATE
SET total_invested = EXCLUDED.total_invested,
nifty_units = EXCLUDED.nifty_units,
gold_units = EXCLUDED.gold_units,
last_sip_ts = EXCLUDED.last_sip_ts,
last_run = EXCLUDED.last_run
""",
(
scope_user,
scope_run,
float(state.get("total_invested", 0.0)),
float(state.get("nifty_units", 0.0)),
float(state.get("gold_units", 0.0)),
last_sip_ts,
last_run,
),
)
if emit_event:
insert_engine_event(
cur,
"STATE_UPDATED",
data={
"mode": "LIVE",
"total_invested": state.get("total_invested"),
"nifty_units": state.get("nifty_units"),
"gold_units": state.get("gold_units"),
"last_sip_ts": state.get("last_sip_ts"),
"last_run": state.get("last_run"),
},
meta=event_meta,
ts=datetime.utcnow().replace(tzinfo=timezone.utc),
)
if cur is not None:
_save(cur)
return
def _op(cur, _conn):
_save(cur)
return run_with_retry(_op)
# engine/state.py
from datetime import datetime, timezone
from indian_paper_trading_strategy.engine.db import db_connection, insert_engine_event, run_with_retry, get_context
DEFAULT_STATE = {
"initial_cash": 0.0,
"cash": 0.0,
"total_invested": 0.0,
"nifty_units": 0.0,
"gold_units": 0.0,
"last_sip_ts": None,
"last_run": None,
"sip_frequency": None,
}
DEFAULT_PAPER_STATE = {
**DEFAULT_STATE,
"initial_cash": 1_000_000.0,
"cash": 1_000_000.0,
"sip_frequency": {"value": 30, "unit": "days"},
}
def _state_key(mode: str | None):
key = (mode or "LIVE").strip().upper()
return "PAPER" if key == "PAPER" else "LIVE"
def _default_state(mode: str | None):
if _state_key(mode) == "PAPER":
return DEFAULT_PAPER_STATE.copy()
return DEFAULT_STATE.copy()
def _local_tz():
return datetime.now().astimezone().tzinfo
def _format_local_ts(value: datetime | None):
if value is None:
return None
return value.astimezone(_local_tz()).replace(tzinfo=None).isoformat()
def _parse_ts(value):
if value is None:
return None
if isinstance(value, datetime):
if value.tzinfo is None:
return value.replace(tzinfo=_local_tz())
return value
if isinstance(value, str):
text = value.strip()
if not text:
return None
try:
parsed = datetime.fromisoformat(text.replace("Z", "+00:00"))
except ValueError:
return None
if parsed.tzinfo is None:
parsed = parsed.replace(tzinfo=_local_tz())
return parsed
return None
def _resolve_scope(user_id: str | None, run_id: str | None):
return get_context(user_id, run_id)
def load_state(
mode: str | None = "LIVE",
*,
cur=None,
for_update: bool = False,
user_id: str | None = None,
run_id: str | None = None,
):
scope_user, scope_run = _resolve_scope(user_id, run_id)
key = _state_key(mode)
if key == "PAPER":
if cur is None:
with db_connection() as conn:
with conn.cursor() as cur:
return load_state(
mode=mode,
cur=cur,
for_update=for_update,
user_id=scope_user,
run_id=scope_run,
)
lock_clause = " FOR UPDATE" if for_update else ""
cur.execute(
f"""
SELECT initial_cash, cash, total_invested, nifty_units, gold_units,
last_sip_ts, last_run, sip_frequency_value, sip_frequency_unit
FROM engine_state_paper
WHERE user_id = %s AND run_id = %s{lock_clause}
LIMIT 1
""",
(scope_user, scope_run),
)
row = cur.fetchone()
if not row:
return _default_state(mode)
merged = _default_state(mode)
merged.update(
{
"initial_cash": float(row[0]) if row[0] is not None else merged["initial_cash"],
"cash": float(row[1]) if row[1] is not None else merged["cash"],
"total_invested": float(row[2]) if row[2] is not None else merged["total_invested"],
"nifty_units": float(row[3]) if row[3] is not None else merged["nifty_units"],
"gold_units": float(row[4]) if row[4] is not None else merged["gold_units"],
"last_sip_ts": _format_local_ts(row[5]),
"last_run": _format_local_ts(row[6]),
}
)
if row[7] is not None or row[8] is not None:
merged["sip_frequency"] = {"value": row[7], "unit": row[8]}
return merged
if cur is None:
with db_connection() as conn:
with conn.cursor() as cur:
return load_state(
mode=mode,
cur=cur,
for_update=for_update,
user_id=scope_user,
run_id=scope_run,
)
lock_clause = " FOR UPDATE" if for_update else ""
cur.execute(
f"""
SELECT total_invested, nifty_units, gold_units, last_sip_ts, last_run
FROM engine_state
WHERE user_id = %s AND run_id = %s{lock_clause}
LIMIT 1
""",
(scope_user, scope_run),
)
row = cur.fetchone()
if not row:
return _default_state(mode)
merged = _default_state(mode)
merged.update(
{
"total_invested": float(row[0]) if row[0] is not None else merged["total_invested"],
"nifty_units": float(row[1]) if row[1] is not None else merged["nifty_units"],
"gold_units": float(row[2]) if row[2] is not None else merged["gold_units"],
"last_sip_ts": _format_local_ts(row[3]),
"last_run": _format_local_ts(row[4]),
}
)
return merged
def init_paper_state(
initial_cash: float,
sip_frequency: dict | None = None,
*,
cur=None,
user_id: str | None = None,
run_id: str | None = None,
):
state = DEFAULT_PAPER_STATE.copy()
state.update(
{
"initial_cash": float(initial_cash),
"cash": float(initial_cash),
"total_invested": 0.0,
"nifty_units": 0.0,
"gold_units": 0.0,
"last_sip_ts": None,
"last_run": None,
"sip_frequency": sip_frequency or state.get("sip_frequency"),
}
)
save_state(state, mode="PAPER", cur=cur, emit_event=True, user_id=user_id, run_id=run_id)
return state
def save_state(
state,
mode: str | None = "LIVE",
*,
cur=None,
emit_event: bool = False,
event_meta: dict | None = None,
user_id: str | None = None,
run_id: str | None = None,
):
scope_user, scope_run = _resolve_scope(user_id, run_id)
key = _state_key(mode)
last_sip_ts = _parse_ts(state.get("last_sip_ts"))
last_run = _parse_ts(state.get("last_run"))
if key == "PAPER":
sip_frequency = state.get("sip_frequency")
sip_value = None
sip_unit = None
if isinstance(sip_frequency, dict):
sip_value = sip_frequency.get("value")
sip_unit = sip_frequency.get("unit")
def _save(cur):
cur.execute(
"""
INSERT INTO engine_state_paper (
user_id, run_id, initial_cash, cash, total_invested, nifty_units, gold_units,
last_sip_ts, last_run, sip_frequency_value, sip_frequency_unit
)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (user_id, run_id) DO UPDATE
SET initial_cash = EXCLUDED.initial_cash,
cash = EXCLUDED.cash,
total_invested = EXCLUDED.total_invested,
nifty_units = EXCLUDED.nifty_units,
gold_units = EXCLUDED.gold_units,
last_sip_ts = EXCLUDED.last_sip_ts,
last_run = EXCLUDED.last_run,
sip_frequency_value = EXCLUDED.sip_frequency_value,
sip_frequency_unit = EXCLUDED.sip_frequency_unit
""",
(
scope_user,
scope_run,
float(state.get("initial_cash", 0.0)),
float(state.get("cash", 0.0)),
float(state.get("total_invested", 0.0)),
float(state.get("nifty_units", 0.0)),
float(state.get("gold_units", 0.0)),
last_sip_ts,
last_run,
sip_value,
sip_unit,
),
)
if emit_event:
insert_engine_event(
cur,
"STATE_UPDATED",
data={
"mode": "PAPER",
"cash": state.get("cash"),
"total_invested": state.get("total_invested"),
"nifty_units": state.get("nifty_units"),
"gold_units": state.get("gold_units"),
"last_sip_ts": state.get("last_sip_ts"),
"last_run": state.get("last_run"),
},
meta=event_meta,
ts=datetime.utcnow().replace(tzinfo=timezone.utc),
)
if cur is not None:
_save(cur)
return
def _op(cur, _conn):
_save(cur)
return run_with_retry(_op)
def _save(cur):
cur.execute(
"""
INSERT INTO engine_state (
user_id, run_id, total_invested, nifty_units, gold_units, last_sip_ts, last_run
)
VALUES (%s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (user_id, run_id) DO UPDATE
SET total_invested = EXCLUDED.total_invested,
nifty_units = EXCLUDED.nifty_units,
gold_units = EXCLUDED.gold_units,
last_sip_ts = EXCLUDED.last_sip_ts,
last_run = EXCLUDED.last_run
""",
(
scope_user,
scope_run,
float(state.get("total_invested", 0.0)),
float(state.get("nifty_units", 0.0)),
float(state.get("gold_units", 0.0)),
last_sip_ts,
last_run,
),
)
if emit_event:
insert_engine_event(
cur,
"STATE_UPDATED",
data={
"mode": "LIVE",
"total_invested": state.get("total_invested"),
"nifty_units": state.get("nifty_units"),
"gold_units": state.get("gold_units"),
"last_sip_ts": state.get("last_sip_ts"),
"last_run": state.get("last_run"),
},
meta=event_meta,
ts=datetime.utcnow().replace(tzinfo=timezone.utc),
)
if cur is not None:
_save(cur)
return
def _op(cur, _conn):
_save(cur)
return run_with_retry(_op)

View File

@ -1,12 +1,12 @@
# engine/strategy.py
import numpy as np
def allocation(sp_price, gd_price, sp_sma, gd_sma,
base=0.6, tilt_mult=1.5,
max_tilt=0.25, min_eq=0.2, max_eq=0.9):
rd = (sp_price / sp_sma) - (gd_price / gd_sma)
tilt = np.clip(-rd * tilt_mult, -max_tilt, max_tilt)
eq_w = np.clip(base * (1 + tilt), min_eq, max_eq)
return eq_w, 1 - eq_w
# engine/strategy.py
import numpy as np
def allocation(sp_price, gd_price, sp_sma, gd_sma,
base=0.6, tilt_mult=1.5,
max_tilt=0.25, min_eq=0.2, max_eq=0.9):
rd = (sp_price / sp_sma) - (gd_price / gd_sma)
tilt = np.clip(-rd * tilt_mult, -max_tilt, max_tilt)
eq_w = np.clip(base * (1 + tilt), min_eq, max_eq)
return eq_w, 1 - eq_w

View File

@ -1,41 +1,41 @@
from datetime import datetime, timedelta
def frequency_to_timedelta(freq: dict) -> timedelta:
value = int(freq.get("value", 0))
unit = freq.get("unit")
if value <= 0:
raise ValueError("Frequency value must be > 0")
if unit == "minutes":
return timedelta(minutes=value)
if unit == "days":
return timedelta(days=value)
raise ValueError(f"Unsupported frequency unit: {unit}")
def normalize_logical_time(ts: datetime) -> datetime:
return ts.replace(microsecond=0)
def compute_logical_time(
now: datetime,
last_run: str | None,
interval_seconds: float | None,
) -> datetime:
base = now
if last_run and interval_seconds:
try:
parsed = datetime.fromisoformat(last_run.replace("Z", "+00:00"))
except ValueError:
parsed = None
if parsed is not None:
if now.tzinfo and parsed.tzinfo is None:
parsed = parsed.replace(tzinfo=now.tzinfo)
elif now.tzinfo is None and parsed.tzinfo:
parsed = parsed.replace(tzinfo=None)
candidate = parsed + timedelta(seconds=interval_seconds)
if now >= candidate:
base = candidate
return normalize_logical_time(base)
from datetime import datetime, timedelta
def frequency_to_timedelta(freq: dict) -> timedelta:
value = int(freq.get("value", 0))
unit = freq.get("unit")
if value <= 0:
raise ValueError("Frequency value must be > 0")
if unit == "minutes":
return timedelta(minutes=value)
if unit == "days":
return timedelta(days=value)
raise ValueError(f"Unsupported frequency unit: {unit}")
def normalize_logical_time(ts: datetime) -> datetime:
return ts.replace(microsecond=0)
def compute_logical_time(
now: datetime,
last_run: str | None,
interval_seconds: float | None,
) -> datetime:
base = now
if last_run and interval_seconds:
try:
parsed = datetime.fromisoformat(last_run.replace("Z", "+00:00"))
except ValueError:
parsed = None
if parsed is not None:
if now.tzinfo and parsed.tzinfo is None:
parsed = parsed.replace(tzinfo=now.tzinfo)
elif now.tzinfo is None and parsed.tzinfo:
parsed = parsed.replace(tzinfo=None)
candidate = parsed + timedelta(seconds=interval_seconds)
if now >= candidate:
base = candidate
return normalize_logical_time(base)

View File

@ -0,0 +1,122 @@
Date,Close
2015-12-31,22.792499542236328
2016-01-31,24.23349952697754
2016-02-29,26.489500045776367
2016-03-31,25.637500762939453
2016-04-30,27.17099952697754
2016-05-31,26.45599937438965
2016-06-30,27.773000717163086
2016-07-31,27.96299934387207
2016-08-31,28.120500564575195
2016-09-30,28.25200080871582
2016-10-31,27.35700035095215
2016-11-30,26.456499099731445
2016-12-31,25.645999908447266
2017-01-31,26.30699920654297
2017-02-28,27.021499633789062
2017-03-31,26.12849998474121
2017-04-30,26.40999984741211
2017-05-31,26.184499740600586
2017-06-30,25.768999099731445
2017-07-31,25.709999084472656
2017-08-31,26.525999069213867
2017-09-30,26.662500381469727
2017-10-31,26.37150001525879
2017-11-30,26.300500869750977
2017-12-31,26.26799964904785
2018-01-31,27.089000701904297
2018-02-28,27.302499771118164
2018-03-31,27.45800018310547
2018-04-30,27.749000549316406
2018-05-31,27.781999588012695
2018-06-30,27.13050079345703
2018-07-31,26.503999710083008
2018-08-31,27.047500610351562
2018-09-30,27.027999877929688
2018-10-31,28.370500564575195
2018-11-30,26.98550033569336
2018-12-31,28.02050018310547
2019-01-31,29.348499298095703
2019-02-28,29.502500534057617
2019-03-31,28.25149917602539
2019-04-30,28.15250015258789
2019-05-31,28.381999969482422
2019-06-30,29.97949981689453
2019-07-31,30.6564998626709
2019-08-31,33.79949951171875
2019-09-30,32.97249984741211
2019-10-31,34.06549835205078
2019-11-30,33.41350173950195
2019-12-31,34.45000076293945
2020-01-31,35.869998931884766
2020-02-29,37.369998931884766
2020-03-31,38.209999084472656
2020-04-30,42.56999969482422
2020-05-31,40.9900016784668
2020-06-30,42.61000061035156
2020-07-31,47.13999938964844
2020-08-31,45.34000015258789
2020-09-30,44.18000030517578
2020-10-31,44.31999969482422
2020-11-30,42.84000015258789
2020-12-31,43.75
2021-01-31,42.650001525878906
2021-02-28,40.25
2021-03-31,38.16999816894531
2021-04-30,40.59000015258789
2021-05-31,42.59000015258789
2021-06-30,40.52000045776367
2021-07-31,41.91999816894531
2021-08-31,40.83000183105469
2021-09-30,39.720001220703125
2021-10-31,41.5
2021-11-30,41.5099983215332
2021-12-31,41.47999954223633
2022-01-31,41.25
2022-02-28,43.59000015258789
2022-03-31,44.119998931884766
2022-04-30,44.790000915527344
2022-05-31,43.95000076293945
2022-06-30,43.7599983215332
2022-07-31,44.060001373291016
2022-08-31,43.900001525878906
2022-09-30,43.15999984741211
2022-10-31,43.22999954223633
2022-11-30,45.099998474121094
2022-12-31,46.790000915527344
2023-01-31,48.66999816894531
2023-02-28,47.40999984741211
2023-03-31,51.06999969482422
2023-04-30,51.25
2023-05-31,51.130001068115234
2023-06-30,49.310001373291016
2023-07-31,50.65999984741211
2023-08-31,50.349998474121094
2023-09-30,49.290000915527344
2023-10-31,52.060001373291016
2023-11-30,53.150001525878906
2023-12-31,53.65999984741211
2024-01-31,53.349998474121094
2024-02-29,52.779998779296875
2024-03-31,56.61000061035156
2024-04-30,60.880001068115234
2024-05-31,61.290000915527344
2024-06-30,60.630001068115234
2024-07-31,58.68000030517578
2024-08-31,60.65999984741211
2024-09-30,63.54999923706055
2024-10-31,66.91999816894531
2024-11-30,64.29000091552734
2024-12-31,64.08999633789062
2025-01-31,69.08000183105469
2025-02-28,71.0999984741211
2025-03-31,74.16000366210938
2025-04-30,79.08999633789062
2025-05-31,79.48999786376953
2025-06-30,79.98999786376953
2025-07-31,82.19999694824219
2025-08-31,85.33999633789062
2025-09-30,95.8499984741211
2025-10-31,100.0199966430664
2025-11-30,104.62000274658203
2025-12-31,114.0999984741211
1 Date Close
2 2015-12-31 22.792499542236328
3 2016-01-31 24.23349952697754
4 2016-02-29 26.489500045776367
5 2016-03-31 25.637500762939453
6 2016-04-30 27.17099952697754
7 2016-05-31 26.45599937438965
8 2016-06-30 27.773000717163086
9 2016-07-31 27.96299934387207
10 2016-08-31 28.120500564575195
11 2016-09-30 28.25200080871582
12 2016-10-31 27.35700035095215
13 2016-11-30 26.456499099731445
14 2016-12-31 25.645999908447266
15 2017-01-31 26.30699920654297
16 2017-02-28 27.021499633789062
17 2017-03-31 26.12849998474121
18 2017-04-30 26.40999984741211
19 2017-05-31 26.184499740600586
20 2017-06-30 25.768999099731445
21 2017-07-31 25.709999084472656
22 2017-08-31 26.525999069213867
23 2017-09-30 26.662500381469727
24 2017-10-31 26.37150001525879
25 2017-11-30 26.300500869750977
26 2017-12-31 26.26799964904785
27 2018-01-31 27.089000701904297
28 2018-02-28 27.302499771118164
29 2018-03-31 27.45800018310547
30 2018-04-30 27.749000549316406
31 2018-05-31 27.781999588012695
32 2018-06-30 27.13050079345703
33 2018-07-31 26.503999710083008
34 2018-08-31 27.047500610351562
35 2018-09-30 27.027999877929688
36 2018-10-31 28.370500564575195
37 2018-11-30 26.98550033569336
38 2018-12-31 28.02050018310547
39 2019-01-31 29.348499298095703
40 2019-02-28 29.502500534057617
41 2019-03-31 28.25149917602539
42 2019-04-30 28.15250015258789
43 2019-05-31 28.381999969482422
44 2019-06-30 29.97949981689453
45 2019-07-31 30.6564998626709
46 2019-08-31 33.79949951171875
47 2019-09-30 32.97249984741211
48 2019-10-31 34.06549835205078
49 2019-11-30 33.41350173950195
50 2019-12-31 34.45000076293945
51 2020-01-31 35.869998931884766
52 2020-02-29 37.369998931884766
53 2020-03-31 38.209999084472656
54 2020-04-30 42.56999969482422
55 2020-05-31 40.9900016784668
56 2020-06-30 42.61000061035156
57 2020-07-31 47.13999938964844
58 2020-08-31 45.34000015258789
59 2020-09-30 44.18000030517578
60 2020-10-31 44.31999969482422
61 2020-11-30 42.84000015258789
62 2020-12-31 43.75
63 2021-01-31 42.650001525878906
64 2021-02-28 40.25
65 2021-03-31 38.16999816894531
66 2021-04-30 40.59000015258789
67 2021-05-31 42.59000015258789
68 2021-06-30 40.52000045776367
69 2021-07-31 41.91999816894531
70 2021-08-31 40.83000183105469
71 2021-09-30 39.720001220703125
72 2021-10-31 41.5
73 2021-11-30 41.5099983215332
74 2021-12-31 41.47999954223633
75 2022-01-31 41.25
76 2022-02-28 43.59000015258789
77 2022-03-31 44.119998931884766
78 2022-04-30 44.790000915527344
79 2022-05-31 43.95000076293945
80 2022-06-30 43.7599983215332
81 2022-07-31 44.060001373291016
82 2022-08-31 43.900001525878906
83 2022-09-30 43.15999984741211
84 2022-10-31 43.22999954223633
85 2022-11-30 45.099998474121094
86 2022-12-31 46.790000915527344
87 2023-01-31 48.66999816894531
88 2023-02-28 47.40999984741211
89 2023-03-31 51.06999969482422
90 2023-04-30 51.25
91 2023-05-31 51.130001068115234
92 2023-06-30 49.310001373291016
93 2023-07-31 50.65999984741211
94 2023-08-31 50.349998474121094
95 2023-09-30 49.290000915527344
96 2023-10-31 52.060001373291016
97 2023-11-30 53.150001525878906
98 2023-12-31 53.65999984741211
99 2024-01-31 53.349998474121094
100 2024-02-29 52.779998779296875
101 2024-03-31 56.61000061035156
102 2024-04-30 60.880001068115234
103 2024-05-31 61.290000915527344
104 2024-06-30 60.630001068115234
105 2024-07-31 58.68000030517578
106 2024-08-31 60.65999984741211
107 2024-09-30 63.54999923706055
108 2024-10-31 66.91999816894531
109 2024-11-30 64.29000091552734
110 2024-12-31 64.08999633789062
111 2025-01-31 69.08000183105469
112 2025-02-28 71.0999984741211
113 2025-03-31 74.16000366210938
114 2025-04-30 79.08999633789062
115 2025-05-31 79.48999786376953
116 2025-06-30 79.98999786376953
117 2025-07-31 82.19999694824219
118 2025-08-31 85.33999633789062
119 2025-09-30 95.8499984741211
120 2025-10-31 100.0199966430664
121 2025-11-30 104.62000274658203
122 2025-12-31 114.0999984741211

View File

@ -0,0 +1,122 @@
Date,Close
2015-12-31,80.3010025024414
2016-01-31,76.22899627685547
2016-02-29,70.83799743652344
2016-03-31,79.12699890136719
2016-04-30,79.51699829101562
2016-05-31,82.66799926757812
2016-06-30,84.28800201416016
2016-07-31,88.11100006103516
2016-08-31,89.61699676513672
2016-09-30,87.85800170898438
2016-10-31,88.17900085449219
2016-11-30,83.91799926757812
2016-12-31,83.48899841308594
2017-01-31,87.22799682617188
2017-02-28,90.66500091552734
2017-03-31,93.8270034790039
2017-04-30,94.98799896240234
2017-05-31,98.22200012207031
2017-06-30,97.5979995727539
2017-07-31,103.427001953125
2017-08-31,102.15299987792969
2017-09-30,100.99800109863281
2017-10-31,106.51699829101562
2017-11-30,105.74600219726562
2017-12-31,108.447998046875
2018-01-31,113.54399871826172
2018-02-28,108.66999816894531
2018-03-31,104.92500305175781
2018-04-30,111.16799926757812
2018-05-31,111.25
2018-06-30,111.18800354003906
2018-07-31,118.25
2018-08-31,121.9000015258789
2018-09-30,114.13999938964844
2018-10-31,108.47599792480469
2018-11-30,113.50800323486328
2018-12-31,113.67500305175781
2019-01-31,113.46900177001953
2019-02-28,113.36000061035156
2019-03-31,120.97699737548828
2019-04-30,123.40699768066406
2019-05-31,125.2490005493164
2019-06-30,124.29199981689453
2019-07-31,117.5770034790039
2019-08-31,116.61399841308594
2019-09-30,121.56700134277344
2019-10-31,125.49800109863281
2019-11-30,127.51300048828125
2019-12-31,129.14999389648438
2020-01-31,127.16999816894531
2020-02-29,119.29000091552734
2020-03-31,91.94000244140625
2020-04-30,104.18000030517578
2020-05-31,101.37999725341797
2020-06-30,109.5199966430664
2020-07-31,118.0199966430664
2020-08-31,121.02999877929688
2020-09-30,119.93000030517578
2020-10-31,124.56999969482422
2020-11-30,138.3800048828125
2020-12-31,149.07000732421875
2021-01-31,146.10000610351562
2021-02-28,156.14999389648438
2021-03-31,157.16000366210938
2021-04-30,157.0
2021-05-31,166.7899932861328
2021-06-30,169.1300048828125
2021-07-31,169.69000244140625
2021-08-31,184.5500030517578
2021-09-30,190.11000061035156
2021-10-31,191.16000366210938
2021-11-30,183.6999969482422
2021-12-31,187.7899932861328
2022-01-31,187.6300048828125
2022-02-28,181.83999633789062
2022-03-31,188.8699951171875
2022-04-30,185.42999267578125
2022-05-31,179.8699951171875
2022-06-30,171.60000610351562
2022-07-31,186.5399932861328
2022-08-31,193.9199981689453
2022-09-30,186.8300018310547
2022-10-31,196.27000427246094
2022-11-30,204.6999969482422
2022-12-31,198.0399932861328
2023-01-31,193.44000244140625
2023-02-28,189.6999969482422
2023-03-31,189.86000061035156
2023-04-30,197.2899932861328
2023-05-31,203.66000366210938
2023-06-30,210.61000061035156
2023-07-31,217.5800018310547
2023-08-31,212.77000427246094
2023-09-30,216.75
2023-10-31,210.97999572753906
2023-11-30,222.1300048828125
2023-12-31,239.64999389648438
2024-01-31,240.25999450683594
2024-02-29,243.25
2024-03-31,246.9600067138672
2024-04-30,250.05999755859375
2024-05-31,251.14999389648438
2024-06-30,267.4800109863281
2024-07-31,277.75
2024-08-31,281.0799865722656
2024-09-30,288.3800048828125
2024-10-31,270.6400146484375
2024-11-30,269.8800048828125
2024-12-31,264.4700012207031
2025-01-31,263.0
2025-02-28,248.4499969482422
2025-03-31,263.2099914550781
2025-04-30,271.989990234375
2025-05-31,277.9700012207031
2025-06-30,286.510009765625
2025-07-31,279.260009765625
2025-08-31,276.05999755859375
2025-09-30,277.5199890136719
2025-10-31,291.0799865722656
2025-11-30,296.489990234375
2025-12-31,294.45001220703125
1 Date Close
2 2015-12-31 80.3010025024414
3 2016-01-31 76.22899627685547
4 2016-02-29 70.83799743652344
5 2016-03-31 79.12699890136719
6 2016-04-30 79.51699829101562
7 2016-05-31 82.66799926757812
8 2016-06-30 84.28800201416016
9 2016-07-31 88.11100006103516
10 2016-08-31 89.61699676513672
11 2016-09-30 87.85800170898438
12 2016-10-31 88.17900085449219
13 2016-11-30 83.91799926757812
14 2016-12-31 83.48899841308594
15 2017-01-31 87.22799682617188
16 2017-02-28 90.66500091552734
17 2017-03-31 93.8270034790039
18 2017-04-30 94.98799896240234
19 2017-05-31 98.22200012207031
20 2017-06-30 97.5979995727539
21 2017-07-31 103.427001953125
22 2017-08-31 102.15299987792969
23 2017-09-30 100.99800109863281
24 2017-10-31 106.51699829101562
25 2017-11-30 105.74600219726562
26 2017-12-31 108.447998046875
27 2018-01-31 113.54399871826172
28 2018-02-28 108.66999816894531
29 2018-03-31 104.92500305175781
30 2018-04-30 111.16799926757812
31 2018-05-31 111.25
32 2018-06-30 111.18800354003906
33 2018-07-31 118.25
34 2018-08-31 121.9000015258789
35 2018-09-30 114.13999938964844
36 2018-10-31 108.47599792480469
37 2018-11-30 113.50800323486328
38 2018-12-31 113.67500305175781
39 2019-01-31 113.46900177001953
40 2019-02-28 113.36000061035156
41 2019-03-31 120.97699737548828
42 2019-04-30 123.40699768066406
43 2019-05-31 125.2490005493164
44 2019-06-30 124.29199981689453
45 2019-07-31 117.5770034790039
46 2019-08-31 116.61399841308594
47 2019-09-30 121.56700134277344
48 2019-10-31 125.49800109863281
49 2019-11-30 127.51300048828125
50 2019-12-31 129.14999389648438
51 2020-01-31 127.16999816894531
52 2020-02-29 119.29000091552734
53 2020-03-31 91.94000244140625
54 2020-04-30 104.18000030517578
55 2020-05-31 101.37999725341797
56 2020-06-30 109.5199966430664
57 2020-07-31 118.0199966430664
58 2020-08-31 121.02999877929688
59 2020-09-30 119.93000030517578
60 2020-10-31 124.56999969482422
61 2020-11-30 138.3800048828125
62 2020-12-31 149.07000732421875
63 2021-01-31 146.10000610351562
64 2021-02-28 156.14999389648438
65 2021-03-31 157.16000366210938
66 2021-04-30 157.0
67 2021-05-31 166.7899932861328
68 2021-06-30 169.1300048828125
69 2021-07-31 169.69000244140625
70 2021-08-31 184.5500030517578
71 2021-09-30 190.11000061035156
72 2021-10-31 191.16000366210938
73 2021-11-30 183.6999969482422
74 2021-12-31 187.7899932861328
75 2022-01-31 187.6300048828125
76 2022-02-28 181.83999633789062
77 2022-03-31 188.8699951171875
78 2022-04-30 185.42999267578125
79 2022-05-31 179.8699951171875
80 2022-06-30 171.60000610351562
81 2022-07-31 186.5399932861328
82 2022-08-31 193.9199981689453
83 2022-09-30 186.8300018310547
84 2022-10-31 196.27000427246094
85 2022-11-30 204.6999969482422
86 2022-12-31 198.0399932861328
87 2023-01-31 193.44000244140625
88 2023-02-28 189.6999969482422
89 2023-03-31 189.86000061035156
90 2023-04-30 197.2899932861328
91 2023-05-31 203.66000366210938
92 2023-06-30 210.61000061035156
93 2023-07-31 217.5800018310547
94 2023-08-31 212.77000427246094
95 2023-09-30 216.75
96 2023-10-31 210.97999572753906
97 2023-11-30 222.1300048828125
98 2023-12-31 239.64999389648438
99 2024-01-31 240.25999450683594
100 2024-02-29 243.25
101 2024-03-31 246.9600067138672
102 2024-04-30 250.05999755859375
103 2024-05-31 251.14999389648438
104 2024-06-30 267.4800109863281
105 2024-07-31 277.75
106 2024-08-31 281.0799865722656
107 2024-09-30 288.3800048828125
108 2024-10-31 270.6400146484375
109 2024-11-30 269.8800048828125
110 2024-12-31 264.4700012207031
111 2025-01-31 263.0
112 2025-02-28 248.4499969482422
113 2025-03-31 263.2099914550781
114 2025-04-30 271.989990234375
115 2025-05-31 277.9700012207031
116 2025-06-30 286.510009765625
117 2025-07-31 279.260009765625
118 2025-08-31 276.05999755859375
119 2025-09-30 277.5199890136719
120 2025-10-31 291.0799865722656
121 2025-11-30 296.489990234375
122 2025-12-31 294.45001220703125

View File

@ -0,0 +1,565 @@
timestamp,event,nifty_units,gold_units,nifty_price,gold_price,amount
2026-01-01T05:58:03.476849,SIP_EXECUTED,12.680485106982822,11.335812442673163,295.7300109863281,110.2699966430664,5000.0
2026-01-01T09:12:36.112648,DEBUG_ENTER_TRY_EXECUTE,2026-01-01T14:42:34.306960
2026-01-01T09:12:36.126094,SIP_EXECUTED,12.692502437171184,11.32041315638739,295.45001220703125,110.41999816894531,5000.0
2026-01-01T09:52:48.760352,DEBUG_ENTER_TRY_EXECUTE,2026-01-01T15:22:48.072786
2026-01-01T09:52:48.764210,DEBUG_EXECUTION_DECISION,True,,2026-01-01T15:22:48.072786
2026-01-01T09:52:48.785706,SIP_EXECUTED,12.695080449700082,11.304033091894532,295.3900146484375,110.58000183105469,5000.0
2026-01-02T04:32:00.780979,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T10:02:00.431831
2026-01-02T04:32:00.862153,DEBUG_EXECUTION_DECISION,True,,2026-01-02T10:02:00.431831
2026-01-02T04:32:00.878460,SIP_EXECUTED,17.695834331967646,15.738825088363468,296.67999267578125,111.19000244140625,7000.0
2026-01-02T05:13:28.371587,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T10:43:28.169205
2026-01-02T05:13:28.382183,DEBUG_EXECUTION_DECISION,True,,2026-01-02T10:43:28.169205
2026-01-02T05:13:28.394370,SIP_EXECUTED,12.636899747262005,11.24201792025962,296.75,111.19000244140625,5000.0
2026-01-02T05:14:45.182100,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T10:44:44.902097
2026-01-02T05:14:45.190620,DEBUG_EXECUTION_DECISION,True,,2026-01-02T10:44:44.902097
2026-01-02T05:14:45.200479,SIP_EXECUTED,12.63647350248328,11.24808810306665,296.760009765625,111.12999725341797,5000.0
2026-01-02T05:17:15.533201,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T10:47:15.253937
2026-01-02T05:17:15.534401,DEBUG_EXECUTION_DECISION,False,2026-01-02T10:44:44.902097,2026-01-02T10:47:15.253937
2026-01-02T05:17:15.546059,SIP_EXECUTED,12.635196239877473,11.24404084364895,296.7900085449219,111.16999816894531,5000.0
2026-01-02T05:19:45.848301,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T10:49:45.609528
2026-01-02T05:19:45.849882,DEBUG_EXECUTION_DECISION,False,2026-01-02T10:47:15.253937,2026-01-02T10:49:45.609528
2026-01-02T05:19:45.864453,SIP_EXECUTED,12.633068475487395,11.24404084364895,296.8399963378906,111.16999816894531,5000.0
2026-01-02T05:22:16.169776,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T10:52:15.920523
2026-01-02T05:22:16.171153,DEBUG_EXECUTION_DECISION,False,2026-01-02T10:49:45.609528,2026-01-02T10:52:15.920523
2026-01-02T05:22:16.182257,SIP_EXECUTED,12.632642489124033,11.236965336252677,296.8500061035156,111.23999786376953,5000.0
2026-01-02T05:24:46.539256,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T10:54:46.241128
2026-01-02T05:24:46.540682,DEBUG_EXECUTION_DECISION,False,2026-01-02T10:52:15.920523,2026-01-02T10:54:46.241128
2026-01-02T05:24:46.555772,SIP_EXECUTED,12.633068475487395,11.229898728027536,296.8399963378906,111.30999755859375,5000.0
2026-01-02T05:27:16.950164,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T10:57:16.609517
2026-01-02T05:27:16.951433,DEBUG_EXECUTION_DECISION,False,2026-01-02T10:54:46.241128,2026-01-02T10:57:16.609517
2026-01-02T05:27:16.965248,SIP_EXECUTED,12.632642489124033,11.231916521851678,296.8500061035156,111.29000091552734,5000.0
2026-01-02T05:29:47.290169,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T10:59:47.029426
2026-01-02T05:29:47.291663,DEBUG_EXECUTION_DECISION,False,2026-01-02T10:57:16.609517,2026-01-02T10:59:47.029426
2026-01-02T05:29:47.307418,SIP_EXECUTED,12.631791901097017,11.231916521851678,296.8699951171875,111.29000091552734,5000.0
2026-01-02T05:32:17.640707,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T11:02:17.367732
2026-01-02T05:32:17.642525,DEBUG_EXECUTION_DECISION,False,2026-01-02T10:59:47.029426,2026-01-02T11:02:17.367732
2026-01-02T05:32:17.661131,SIP_EXECUTED,12.62923952614863,11.230907149379977,296.92999267578125,111.30000305175781,5000.0
2026-01-02T05:34:47.871879,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T11:04:47.726590
2026-01-02T05:34:47.873422,DEBUG_EXECUTION_DECISION,False,2026-01-02T11:02:17.367732,2026-01-02T11:04:47.726590
2026-01-02T05:34:47.887928,SIP_EXECUTED,12.630515584676825,4.209887994984313,296.8999938964844,296.9200134277344,5000.0
2026-01-02T05:37:49.447179,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T11:07:49.194030
2026-01-02T05:37:49.450689,DEBUG_EXECUTION_DECISION,True,,2026-01-02T11:07:49.194030
2026-01-02T05:37:49.461281,SIP_EXECUTED,12.630089770459836,11.230907149379977,296.9100036621094,111.30000305175781,5000.0
2026-01-02T05:37:59.197358,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T11:07:58.950032
2026-01-02T05:37:59.199218,DEBUG_EXECUTION_DECISION,True,,2026-01-02T11:07:58.950032
2026-01-02T05:37:59.209559,SIP_EXECUTED,12.630089770459836,11.229898728027536,296.9100036621094,111.30999755859375,5000.0
2026-01-02T05:40:29.519965,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T11:10:29.266583
2026-01-02T05:40:29.521195,DEBUG_EXECUTION_DECISION,False,2026-01-02T11:07:58.950032,2026-01-02T11:10:29.266583
2026-01-02T05:40:29.534463,SIP_EXECUTED,12.640306840156798,11.229898728027536,296.6700134277344,111.30999755859375,5000.0
2026-01-02T05:42:59.789448,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T11:12:59.581871
2026-01-02T05:42:59.790893,DEBUG_EXECUTION_DECISION,False,2026-01-02T11:10:29.266583,2026-01-02T11:12:59.581871
2026-01-02T05:42:59.806339,SIP_EXECUTED,12.634771409141003,11.229898728027536,296.79998779296875,111.30999755859375,5000.0
2026-01-02T05:45:30.148210,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T11:15:29.873692
2026-01-02T05:45:30.149482,DEBUG_EXECUTION_DECISION,False,2026-01-02T11:12:59.581871,2026-01-02T11:15:29.873692
2026-01-02T05:45:31.939212,SIP_EXECUTED,12.630515584676825,11.229898728027536,296.8999938964844,111.30999755859375,5000.0
2026-01-02T05:48:02.251839,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T11:18:01.987772
2026-01-02T05:48:02.253162,DEBUG_EXECUTION_DECISION,False,2026-01-02T11:15:29.873692,2026-01-02T11:18:01.987772
2026-01-02T05:48:02.266372,SIP_EXECUTED,12.630515584676825,11.230907149379977,296.8999938964844,111.30000305175781,5000.0
2026-01-02T05:50:32.781667,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T11:20:32.524156
2026-01-02T05:50:32.783021,DEBUG_EXECUTION_DECISION,False,2026-01-02T11:18:01.987772,2026-01-02T11:20:32.524156
2026-01-02T05:50:32.797277,SIP_EXECUTED,12.628388098484162,11.228889718170905,296.95001220703125,111.31999969482422,5000.0
2026-01-02T05:53:03.093498,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T11:23:02.852193
2026-01-02T05:53:03.095076,DEBUG_EXECUTION_DECISION,False,2026-01-02T11:20:32.524156,2026-01-02T11:23:02.852193
2026-01-02T05:53:03.115518,SIP_EXECUTED,12.627538083259335,11.225864545391198,296.9700012207031,111.3499984741211,5000.0
2026-01-02T05:55:35.173087,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T11:25:34.906273
2026-01-02T05:55:35.174639,DEBUG_EXECUTION_DECISION,False,2026-01-02T11:23:02.852193,2026-01-02T11:25:34.906273
2026-01-02T05:55:35.188199,SIP_EXECUTED,12.627538083259335,11.223848156350726,296.9700012207031,111.37000274658203,5000.0
2026-01-02T05:58:05.581589,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T11:28:05.247300
2026-01-02T05:58:05.583163,DEBUG_EXECUTION_DECISION,False,2026-01-02T11:25:34.906273,2026-01-02T11:28:05.247300
2026-01-02T05:58:05.599412,SIP_EXECUTED,12.626262626262626,11.224856260316914,297.0,111.36000061035156,5000.0
2026-01-02T06:00:36.027342,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T11:30:35.671929
2026-01-02T06:00:36.028705,DEBUG_EXECUTION_DECISION,False,2026-01-02T11:28:05.247300,2026-01-02T11:30:35.671929
2026-01-02T06:00:36.044440,SIP_EXECUTED,12.626262626262626,11.22687301162239,297.0,111.33999633789062,5000.0
2026-01-02T06:03:06.408278,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T11:33:06.101772
2026-01-02T06:03:06.410821,DEBUG_EXECUTION_DECISION,False,2026-01-02T11:30:35.671929,2026-01-02T11:33:06.101772
2026-01-02T06:03:06.427684,SIP_EXECUTED,12.626262626262626,11.22687301162239,297.0,111.33999633789062,5000.0
2026-01-02T06:26:35.161177,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T11:56:34.868541
2026-01-02T06:26:35.166307,DEBUG_EXECUTION_DECISION,True,,2026-01-02T11:56:34.868541
2026-01-02T06:26:35.190868,SIP_EXECUTED,12.61309754395646,11.227880889617678,297.30999755859375,111.33000183105469,5000.0
2026-01-02T06:29:05.546031,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T11:59:05.227324
2026-01-02T06:29:05.547409,DEBUG_EXECUTION_DECISION,False,2026-01-02T11:56:34.868541,2026-01-02T11:59:05.227324
2026-01-02T06:29:05.573435,SIP_EXECUTED,12.615643397813288,11.227880889617678,297.25,111.33000183105469,5000.0
2026-01-02T06:31:36.123447,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:01:35.709182
2026-01-02T06:31:36.124647,DEBUG_EXECUTION_DECISION,False,2026-01-02T11:59:05.227324,2026-01-02T12:01:35.709182
2026-01-02T06:31:36.150934,SIP_EXECUTED,12.61861529181472,11.228889718170905,297.17999267578125,111.31999969482422,5000.0
2026-01-02T06:34:06.331380,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:04:06.192255
2026-01-02T06:34:06.332732,DEBUG_EXECUTION_DECISION,False,2026-01-02T12:01:35.709182,2026-01-02T12:04:06.192255
2026-01-02T06:34:08.079260,SIP_EXECUTED,12.618190279598476,11.228889718170905,297.19000244140625,111.31999969482422,5000.0
2026-01-02T06:36:38.523647,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:06:38.125235
2026-01-02T06:36:38.524980,DEBUG_EXECUTION_DECISION,False,2026-01-02T12:04:06.192255,2026-01-02T12:06:38.125235
2026-01-02T06:36:38.560698,SIP_EXECUTED,12.614370342432728,11.227880889617678,297.2799987792969,111.33000183105469,5000.0
2026-01-02T06:39:08.891753,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:09:08.608700
2026-01-02T06:39:08.893090,DEBUG_EXECUTION_DECISION,False,2026-01-02T12:06:38.125235,2026-01-02T12:09:08.608700
2026-01-02T06:39:08.919794,SIP_EXECUTED,12.613945616114972,11.22687301162239,297.2900085449219,111.33999633789062,5000.0
2026-01-02T06:41:39.672495,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:11:38.965816
2026-01-02T06:41:39.673763,DEBUG_EXECUTION_DECISION,False,2026-01-02T12:09:08.608700,2026-01-02T12:11:38.965816
2026-01-02T06:41:39.698935,SIP_EXECUTED,12.614795097353511,11.230907149379977,297.2699890136719,111.30000305175781,5000.0
2026-01-02T06:44:10.070501,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:14:09.744877
2026-01-02T06:44:10.071848,DEBUG_EXECUTION_DECISION,False,2026-01-02T12:11:38.965816,2026-01-02T12:14:09.744877
2026-01-02T06:44:10.093468,SIP_EXECUTED,12.61309754395646,11.231916521851678,297.30999755859375,111.29000091552734,5000.0
2026-01-02T06:46:40.576767,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:16:40.141259
2026-01-02T06:46:40.578178,DEBUG_EXECUTION_DECISION,False,2026-01-02T12:14:09.744877,2026-01-02T12:16:40.141259
2026-01-02T06:46:40.602665,SIP_EXECUTED,12.613945616114972,11.231916521851678,297.2900085449219,111.29000091552734,5000.0
2026-01-02T06:49:10.983710,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:19:10.649167
2026-01-02T06:49:10.985361,DEBUG_EXECUTION_DECISION,False,2026-01-02T12:16:40.141259,2026-01-02T12:19:10.649167
2026-01-02T06:49:11.013758,SIP_EXECUTED,12.61606823847326,11.230907149379977,297.239990234375,111.30000305175781,5000.0
2026-01-02T06:51:41.373876,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:21:41.098025
2026-01-02T06:52:11.790140,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:22:11.407495
2026-01-02T06:52:42.118673,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:22:41.821207
2026-01-02T06:53:12.335393,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:23:12.151146
2026-01-02T06:53:42.620830,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:23:42.355737
2026-01-02T06:54:12.966364,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:24:12.654653
2026-01-02T06:54:43.174257,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:24:42.987309
2026-01-02T06:55:13.406894,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:25:13.209279
2026-01-02T06:55:43.865567,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:25:43.444555
2026-01-02T06:56:14.169655,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:26:13.911016
2026-01-02T06:56:44.634522,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:26:44.190515
2026-01-02T06:57:14.871879,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:27:14.683277
2026-01-02T06:57:45.159095,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:27:44.897628
2026-01-02T06:58:15.419866,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:28:15.194239
2026-01-02T06:58:45.754988,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:28:45.442942
2026-01-02T06:59:16.044665,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:29:15.790301
2026-01-02T06:59:46.317902,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:29:46.068587
2026-01-02T07:00:16.503189,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:30:16.369723
2026-01-02T07:00:46.778131,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:30:46.524842
2026-01-02T07:01:17.131399,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:31:16.812978
2026-01-02T07:01:47.466590,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:31:47.153432
2026-01-02T07:02:17.772282,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:32:17.501504
2026-01-02T07:02:48.160836,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:32:47.794088
2026-01-02T07:03:18.689219,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:33:18.218594
2026-01-02T07:03:49.174720,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:33:48.713810
2026-01-02T07:04:19.392362,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:34:19.212162
2026-01-02T07:04:49.754539,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:34:49.425794
2026-01-02T07:05:20.037005,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:35:19.804851
2026-01-02T07:05:50.227435,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:35:50.060712
2026-01-02T07:06:20.621828,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:36:20.264642
2026-01-02T07:06:50.925277,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:36:50.648097
2026-01-02T07:07:21.229912,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:37:20.960328
2026-01-02T07:07:51.393719,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:37:51.255164
2026-01-02T07:08:21.763821,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:38:21.564796
2026-01-02T07:08:52.117993,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:38:51.788257
2026-01-02T07:09:22.442868,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:39:22.172548
2026-01-02T07:09:52.759654,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:39:52.466182
2026-01-02T07:10:23.186163,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:40:22.799263
2026-01-02T07:10:53.435155,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:40:53.210715
2026-01-02T07:11:23.678565,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:41:23.483857
2026-01-02T07:11:54.171460,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:41:53.705566
2026-01-02T07:12:24.493696,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:42:24.234953
2026-01-02T07:12:54.774296,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:42:54.525260
2026-01-02T07:13:25.102794,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:43:24.825793
2026-01-02T07:13:55.521540,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:43:55.139089
2026-01-02T07:14:25.859124,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:44:25.574597
2026-01-02T07:14:56.245862,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:44:55.890082
2026-01-02T07:15:26.698632,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:45:26.319891
2026-01-02T07:15:56.943886,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:45:56.726904
2026-01-02T07:16:27.390504,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:46:27.139781
2026-01-02T07:16:57.763934,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:46:57.446567
2026-01-02T07:17:28.177361,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:47:27.831689
2026-01-02T07:17:58.546708,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:47:58.203855
2026-01-02T07:18:29.107084,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:48:28.600134
2026-01-02T07:18:59.476645,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:48:59.146827
2026-01-02T07:19:29.862600,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:49:29.522965
2026-01-02T07:20:00.486506,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:49:59.891571
2026-01-02T07:20:31.022694,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:50:30.533620
2026-01-02T07:21:01.362285,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:51:01.055094
2026-01-02T07:21:31.742290,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:51:31.410211
2026-01-02T07:22:02.258084,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:52:01.784392
2026-01-02T07:22:32.650896,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:52:32.293440
2026-01-02T07:23:03.138776,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:53:02.685434
2026-01-02T07:23:33.441563,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:53:33.174619
2026-01-02T07:24:03.857707,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:54:03.504691
2026-01-02T07:24:34.291862,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:54:33.887700
2026-01-02T07:25:04.553563,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:55:04.315279
2026-01-02T07:25:34.923003,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:55:34.674190
2026-01-02T07:26:05.260020,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:56:04.947055
2026-01-02T07:26:35.600635,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:56:35.288578
2026-01-02T07:27:06.027215,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:57:05.744054
2026-01-02T07:27:36.398120,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:57:36.057056
2026-01-02T07:28:06.928343,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:58:06.598661
2026-01-02T07:28:37.215033,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:58:36.958288
2026-01-02T07:29:07.610609,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:59:07.257368
2026-01-02T07:29:38.007012,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T12:59:37.638780
2026-01-02T07:30:08.387299,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T13:00:08.051545
2026-01-02T07:30:39.223424,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T13:00:38.823157
2026-01-02T07:31:09.557524,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T13:01:09.271203
2026-01-02T07:31:39.926100,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T13:01:39.598421
2026-01-02T07:32:10.377919,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T13:02:09.978735
2026-01-02T07:32:40.736737,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T13:02:40.412221
2026-01-02T07:33:11.011490,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T13:03:10.763256
2026-01-02T07:33:41.304918,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T13:03:41.048147
2026-01-02T07:34:11.668584,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T13:04:11.352171
2026-01-02T07:34:41.976797,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T13:04:41.704478
2026-01-02T07:35:12.358022,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T13:05:12.026707
2026-01-02T07:35:42.769252,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T13:05:42.392460
2026-01-02T07:36:13.192041,DEBUG_ENTER_TRY_EXECUTE,2026-01-02T13:06:12.917031
2026-01-07T04:27:43.410112,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T09:57:41.269722
2026-01-07T04:27:43.852337,DEBUG_EXECUTION_DECISION,True,,2026-01-07T09:57:41.269722
2026-01-07T04:27:43.915086,SIP_EXECUTED,12.691213823508058,11.046305757752034,295.4800109863281,113.16000366210938,5000.0
2026-01-07T04:38:14.823897,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T10:08:14.318325
2026-01-07T04:38:14.825782,DEBUG_EXECUTION_DECISION,False,2026-01-07T09:57:41.269722,2026-01-07T10:08:14.318325
2026-01-07T04:38:14.844313,SIP_EXECUTED,12.682630184288277,11.050211895771628,295.67999267578125,113.12000274658203,5000.0
2026-01-07T04:48:45.795744,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T10:18:45.352278
2026-01-07T04:48:45.799100,DEBUG_EXECUTION_DECISION,False,2026-01-07T10:08:14.318325,2026-01-07T10:18:45.352278
2026-01-07T04:48:45.837197,SIP_EXECUTED,12.66891891891892,11.05412079730545,296.0,113.08000183105469,5000.0
2026-01-07T04:59:18.402470,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T10:29:17.772055
2026-01-07T04:59:18.417897,DEBUG_EXECUTION_DECISION,False,2026-01-07T10:18:45.352278,2026-01-07T10:29:17.772055
2026-01-07T04:59:18.444827,SIP_EXECUTED,12.690355329949238,11.066844035177757,295.5,112.94999694824219,5000.0
2026-01-07T05:09:49.045622,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T10:39:48.603141
2026-01-07T05:09:49.070372,DEBUG_EXECUTION_DECISION,False,2026-01-07T10:29:17.772055,2026-01-07T10:39:48.603141
2026-01-07T05:09:49.096436,SIP_EXECUTED,12.688208949028466,11.057054102225111,295.54998779296875,113.05000305175781,5000.0
2026-01-07T05:20:19.685590,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T10:50:19.249841
2026-01-07T05:20:19.708210,DEBUG_EXECUTION_DECISION,False,2026-01-07T10:39:48.603141,2026-01-07T10:50:19.249841
2026-01-07T05:20:19.732297,SIP_EXECUTED,12.683487632928637,11.043378152121456,295.6600036621094,113.19000244140625,5000.0
2026-01-07T05:30:50.511328,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T11:00:49.884030
2026-01-07T05:30:50.526937,DEBUG_EXECUTION_DECISION,False,2026-01-07T10:50:19.249841,2026-01-07T11:00:49.884030
2026-01-07T05:30:50.551788,SIP_EXECUTED,12.673200666951937,11.042403124547173,295.8999938964844,113.19999694824219,5000.0
2026-01-07T05:41:20.968460,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T11:11:20.698065
2026-01-07T05:41:20.985144,DEBUG_EXECUTION_DECISION,False,2026-01-07T11:00:49.884030,2026-01-07T11:11:20.698065
2026-01-07T05:41:21.146260,SIP_EXECUTED,12.671487359139526,11.074687424315252,295.94000244140625,112.87000274658203,5000.0
2026-01-07T05:51:53.585313,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T11:21:51.313563
2026-01-07T05:51:53.602346,DEBUG_EXECUTION_DECISION,False,2026-01-07T11:11:20.698065,2026-01-07T11:21:51.313563
2026-01-07T05:51:53.625518,SIP_EXECUTED,12.685633726276894,11.087458077748499,295.6099853515625,112.73999786376953,5000.0
2026-01-07T06:02:24.232218,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T11:32:23.785980
2026-01-07T06:02:24.254325,DEBUG_EXECUTION_DECISION,False,2026-01-07T11:21:51.313563,2026-01-07T11:32:23.785980
2026-01-07T06:02:24.278381,SIP_EXECUTED,12.684774678018549,11.0795958463147,295.6300048828125,112.81999969482422,5000.0
2026-01-07T06:12:54.723593,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T11:42:54.450207
2026-01-07T06:12:54.745226,DEBUG_EXECUTION_DECISION,False,2026-01-07T11:32:23.785980,2026-01-07T11:42:54.450207
2026-01-07T06:12:54.773565,SIP_EXECUTED,12.68177154275691,11.075668910508135,295.70001220703125,112.86000061035156,5000.0
2026-01-07T06:23:25.229770,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T11:53:24.970738
2026-01-07T06:23:25.248930,DEBUG_EXECUTION_DECISION,False,2026-01-07T11:42:54.450207,2026-01-07T11:53:24.970738
2026-01-07T06:23:25.278476,SIP_EXECUTED,12.690355329949238,11.072725722014841,295.5,112.88999938964844,5000.0
2026-01-07T06:33:59.593423,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T12:03:58.936167
2026-01-07T06:33:59.653147,DEBUG_EXECUTION_DECISION,False,2026-01-07T11:53:24.970738,2026-01-07T12:03:58.936167
2026-01-07T06:33:59.684138,SIP_EXECUTED,12.692502437171184,11.094346501414353,295.45001220703125,112.66999816894531,5000.0
2026-01-07T06:44:30.282688,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T12:14:29.864185
2026-01-07T06:44:32.021721,DEBUG_EXECUTION_DECISION,False,2026-01-07T12:03:58.936167,2026-01-07T12:14:29.864185
2026-01-07T06:44:32.046363,SIP_EXECUTED,12.693791312541947,11.094346501414353,295.4200134277344,112.66999816894531,5000.0
2026-01-07T06:55:02.637614,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T12:25:02.274159
2026-01-07T06:55:02.656508,DEBUG_EXECUTION_DECISION,False,2026-01-07T12:14:29.864185,2026-01-07T12:25:02.274159
2026-01-07T06:55:02.690725,SIP_EXECUTED,12.695510657492727,11.102229688760808,295.3800048828125,112.58999633789062,5000.0
2026-01-07T07:05:39.591389,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T12:35:37.448636
2026-01-07T07:05:39.608519,DEBUG_EXECUTION_DECISION,False,2026-01-07T12:25:02.274159,2026-01-07T12:35:37.448636
2026-01-07T07:05:39.632513,SIP_EXECUTED,12.697230468269272,11.098286695217569,295.3399963378906,112.62999725341797,5000.0
2026-01-07T07:16:10.323455,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T12:46:09.830294
2026-01-07T07:16:10.340861,DEBUG_EXECUTION_DECISION,False,2026-01-07T12:35:37.448636,2026-01-07T12:46:09.830294
2026-01-07T07:16:10.366530,SIP_EXECUTED,12.70712678709482,11.091393379310084,295.1099853515625,112.69999694824219,5000.0
2026-01-07T07:26:40.923012,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T12:56:40.579703
2026-01-07T07:26:40.943008,DEBUG_EXECUTION_DECISION,False,2026-01-07T12:46:09.830294,2026-01-07T12:56:40.579703
2026-01-07T07:26:40.974290,SIP_EXECUTED,12.701100762066046,11.076650570683451,295.25,112.8499984741211,5000.0
2026-01-07T07:37:11.472478,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:07:11.183080
2026-01-07T07:37:13.284403,DEBUG_EXECUTION_DECISION,False,2026-01-07T12:56:40.579703,2026-01-07T13:07:11.183080
2026-01-07T07:37:13.315846,SIP_EXECUTED,12.694221432965616,11.068803653786144,295.4100036621094,112.93000030517578,5000.0
2026-01-07T07:47:43.815060,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:17:43.535913
2026-01-07T07:47:45.558607,DEBUG_EXECUTION_DECISION,False,2026-01-07T13:07:11.183080,2026-01-07T13:17:43.535913
2026-01-07T07:47:45.580616,SIP_EXECUTED,12.6916437692736,11.063904787211952,295.4700012207031,112.9800033569336,5000.0
2026-01-07T07:58:16.115113,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:28:15.804225
2026-01-07T07:58:46.424701,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:28:46.174050
2026-01-07T07:59:18.534483,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:29:16.469860
2026-01-07T07:59:48.861594,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:29:48.570905
2026-01-07T08:00:19.288717,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:30:19.014374
2026-01-07T08:00:49.676268,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:30:49.328975
2026-01-07T08:01:21.724369,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:31:21.445475
2026-01-07T08:01:52.073009,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:31:51.760682
2026-01-07T08:02:22.408611,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:32:22.138983
2026-01-07T08:02:52.746034,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:32:52.445855
2026-01-07T08:03:23.390341,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:33:22.799139
2026-01-07T08:03:54.079204,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:33:53.430586
2026-01-07T08:04:24.422263,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:34:24.136738
2026-01-07T08:04:54.744168,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:34:54.457071
2026-01-07T08:05:25.108712,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:35:24.805661
2026-01-07T08:05:55.408949,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:35:55.144071
2026-01-07T08:06:25.724394,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:36:25.462751
2026-01-07T08:06:56.104705,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:36:55.759933
2026-01-07T08:07:26.435872,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:37:26.155692
2026-01-07T08:07:56.744751,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:37:56.480207
2026-01-07T08:08:27.118561,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:38:26.797479
2026-01-07T08:08:57.674969,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:38:57.171264
2026-01-07T08:09:28.018561,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:39:27.726367
2026-01-07T08:09:58.350496,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:39:58.054571
2026-01-07T08:10:28.693027,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:40:28.408853
2026-01-07T08:10:59.009310,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:40:58.733310
2026-01-07T08:11:29.319198,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:41:29.060958
2026-01-07T08:11:59.730676,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:41:59.356325
2026-01-07T08:12:30.362071,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:42:29.780721
2026-01-07T08:13:00.755914,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:43:00.401318
2026-01-07T08:13:31.093855,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:43:30.819173
2026-01-07T08:14:01.406035,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:44:01.132816
2026-01-07T08:14:31.841933,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:44:31.464296
2026-01-07T08:15:02.161215,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:45:01.880764
2026-01-07T08:15:32.520193,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:45:32.227531
2026-01-07T08:16:02.822837,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:46:02.562851
2026-01-07T08:16:33.366141,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:46:32.874561
2026-01-07T08:17:04.011011,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:47:03.404636
2026-01-07T08:17:34.678585,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:47:34.151609
2026-01-07T08:18:04.974493,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:48:04.716494
2026-01-07T08:18:35.684084,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:48:35.029121
2026-01-07T08:19:06.240150,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:49:05.725947
2026-01-07T08:19:36.648678,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:49:36.291371
2026-01-07T08:20:07.024704,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:50:06.691356
2026-01-07T08:20:37.556485,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:50:37.077851
2026-01-07T08:21:07.993128,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:51:07.595444
2026-01-07T08:21:38.324194,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:51:38.046784
2026-01-07T08:22:08.629739,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:52:08.371315
2026-01-07T08:22:39.243840,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:52:38.767765
2026-01-07T08:23:09.757149,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:53:09.282615
2026-01-07T08:23:40.215913,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:53:39.919640
2026-01-07T08:24:10.528558,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:54:10.254210
2026-01-07T08:24:40.901119,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:54:40.583353
2026-01-07T08:25:11.199803,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:55:10.945357
2026-01-07T08:25:41.659920,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:55:41.348222
2026-01-07T08:26:13.844706,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:56:13.428835
2026-01-07T08:26:44.264391,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:56:43.908736
2026-01-07T08:27:14.612764,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:57:14.306402
2026-01-07T08:27:45.354303,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:57:44.764489
2026-01-07T08:28:15.994169,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:58:15.399751
2026-01-07T08:28:46.630481,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:58:46.047817
2026-01-07T08:29:16.961290,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:59:16.671282
2026-01-07T08:29:47.441184,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T13:59:47.150324
2026-01-07T08:30:17.785286,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:00:17.491260
2026-01-07T08:30:48.156252,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:00:47.840295
2026-01-07T08:31:18.484226,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:01:18.198936
2026-01-07T08:31:48.838503,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:01:48.539878
2026-01-07T08:32:19.157723,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:02:18.882952
2026-01-07T08:32:49.480273,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:02:49.212114
2026-01-07T08:33:19.855629,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:03:19.522932
2026-01-07T08:33:50.183652,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:03:49.919795
2026-01-07T08:34:20.962486,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:04:20.225359
2026-01-07T08:34:51.409359,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:04:51.027480
2026-01-07T08:35:21.735025,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:05:21.450670
2026-01-07T08:35:52.058188,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:05:51.789860
2026-01-07T08:36:22.400106,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:06:22.099387
2026-01-07T08:36:52.759345,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:06:52.455844
2026-01-07T08:37:23.126355,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:07:22.802439
2026-01-07T08:37:53.479056,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:07:53.181533
2026-01-07T08:38:23.987404,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:08:23.528039
2026-01-07T08:38:54.353985,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:08:54.044139
2026-01-07T08:39:24.711718,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:09:24.402128
2026-01-07T08:39:55.053816,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:09:54.770011
2026-01-07T08:40:25.385180,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:10:25.095913
2026-01-07T08:40:55.810395,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:10:55.440501
2026-01-07T08:41:26.183877,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:11:25.856725
2026-01-07T08:41:56.537924,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:11:56.239743
2026-01-07T08:42:26.944636,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:12:26.589402
2026-01-07T08:42:57.274606,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:12:57.000722
2026-01-07T08:43:27.597799,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:13:27.316210
2026-01-07T08:43:57.931465,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:13:57.661041
2026-01-07T08:44:28.312223,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:14:27.972810
2026-01-07T08:44:58.625759,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:14:58.368307
2026-01-07T08:45:28.979282,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:15:28.671262
2026-01-07T08:45:59.417157,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:15:59.041164
2026-01-07T08:46:29.826278,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:16:29.460997
2026-01-07T08:47:00.151270,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:16:59.891626
2026-01-07T08:47:30.484212,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:17:30.194911
2026-01-07T08:48:00.844502,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:18:00.552045
2026-01-07T08:48:31.140554,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:18:30.888290
2026-01-07T08:49:01.471630,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:19:01.199970
2026-01-07T08:49:31.799281,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:19:31.514720
2026-01-07T08:50:04.208560,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:20:03.615125
2026-01-07T08:50:34.559800,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:20:34.253256
2026-01-07T08:51:06.600006,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:21:06.334980
2026-01-07T08:51:36.981380,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:21:36.644931
2026-01-07T08:52:09.184249,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:22:08.831730
2026-01-07T08:52:39.531825,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:22:39.227037
2026-01-07T08:53:09.859882,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:23:09.589167
2026-01-07T08:53:40.188253,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:23:39.905935
2026-01-07T08:54:10.543299,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:24:10.256479
2026-01-07T08:54:40.916994,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:24:40.591786
2026-01-07T08:55:11.248214,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:25:10.983371
2026-01-07T08:55:41.566688,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:25:41.291380
2026-01-07T08:56:11.908030,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:26:11.625105
2026-01-07T08:56:42.201972,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:26:41.950612
2026-01-07T08:57:12.521573,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:27:12.260099
2026-01-07T08:57:42.871067,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:27:42.566689
2026-01-07T08:58:15.152919,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:28:14.645086
2026-01-07T08:58:45.711497,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:28:45.201672
2026-01-07T08:59:16.612523,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:29:16.095113
2026-01-07T08:59:47.237259,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:29:46.664787
2026-01-07T09:00:17.580568,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:30:17.295333
2026-01-07T09:00:47.996846,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:30:47.639789
2026-01-07T09:01:18.656240,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:31:18.055990
2026-01-07T09:01:49.195849,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:31:48.704628
2026-01-07T09:02:19.626526,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:32:19.262928
2026-01-07T09:02:50.017395,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:32:49.673335
2026-01-07T09:03:20.446662,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:33:20.086593
2026-01-07T09:03:50.779637,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:33:50.496507
2026-01-07T09:04:21.133853,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:34:20.844466
2026-01-07T09:04:51.464747,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:34:51.186579
2026-01-07T09:05:21.839527,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:35:21.534288
2026-01-07T09:05:52.179462,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:35:51.886434
2026-01-07T09:06:22.773013,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:36:22.244837
2026-01-07T09:06:53.449669,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:36:52.822286
2026-01-07T09:07:24.002988,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:37:23.515253
2026-01-07T09:07:54.579479,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:37:54.052938
2026-01-07T09:08:24.930486,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:38:24.637657
2026-01-07T09:08:55.323677,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:38:54.982773
2026-01-07T09:09:27.544804,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:39:27.112878
2026-01-07T09:09:57.938271,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:39:57.592552
2026-01-07T09:10:28.291105,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:40:27.998406
2026-01-07T09:10:58.658052,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:40:58.339090
2026-01-07T09:11:29.081308,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:41:28.726271
2026-01-07T09:11:59.786360,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:41:59.128578
2026-01-07T09:12:30.130825,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:42:29.850113
2026-01-07T09:13:00.462803,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:43:00.177193
2026-01-07T09:13:30.885079,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:43:30.524942
2026-01-07T09:14:01.222388,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:44:00.937083
2026-01-07T09:14:31.575419,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:44:31.285756
2026-01-07T09:15:01.913234,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:45:01.625844
2026-01-07T09:15:32.460794,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:45:32.072191
2026-01-07T09:16:02.813897,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:46:02.521637
2026-01-07T09:16:33.183106,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:46:32.874048
2026-01-07T09:17:03.708554,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:47:03.235566
2026-01-07T09:17:34.106918,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:47:33.770560
2026-01-07T09:18:04.446300,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:48:04.155842
2026-01-07T09:18:34.828131,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:48:34.508970
2026-01-07T09:19:05.179299,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:49:04.882488
2026-01-07T09:19:35.727847,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:49:35.341689
2026-01-07T09:20:06.267859,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:50:05.781907
2026-01-07T09:20:36.629931,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:50:36.329453
2026-01-07T09:21:06.978332,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:51:06.677249
2026-01-07T09:21:37.419406,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:51:37.047947
2026-01-07T09:22:07.790143,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:52:07.465936
2026-01-07T09:22:39.950401,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:52:39.568251
2026-01-07T09:23:10.277009,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:53:10.005315
2026-01-07T09:23:40.687636,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:53:40.339480
2026-01-07T09:24:13.101719,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:54:12.808714
2026-01-07T09:24:43.561077,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:54:43.163907
2026-01-07T09:25:13.886679,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:55:13.613283
2026-01-07T09:25:44.237917,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:55:43.949576
2026-01-07T09:26:14.601706,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:56:14.289832
2026-01-07T09:26:45.098096,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:56:44.674862
2026-01-07T09:27:15.552523,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:57:15.149128
2026-01-07T09:27:45.988996,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:57:45.627468
2026-01-07T09:28:16.409822,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:58:16.039488
2026-01-07T09:28:46.763459,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:58:46.481463
2026-01-07T09:29:17.208394,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:59:16.813767
2026-01-07T09:29:47.664059,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T14:59:47.276893
2026-01-07T09:30:18.203187,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:00:17.717498
2026-01-07T09:30:48.554331,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:00:48.278536
2026-01-07T09:31:18.888211,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:01:18.607727
2026-01-07T09:31:49.320761,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:01:48.967801
2026-01-07T09:32:19.685527,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:02:19.374123
2026-01-07T09:32:50.066756,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:02:49.763777
2026-01-07T09:33:20.546026,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:03:20.117121
2026-01-07T09:33:50.924119,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:03:50.629608
2026-01-07T09:34:21.286143,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:04:20.975939
2026-01-07T09:34:51.838758,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:04:51.457034
2026-01-07T09:35:22.173581,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:05:21.893830
2026-01-07T09:35:52.658003,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:05:52.244543
2026-01-07T09:36:23.126948,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:06:22.724023
2026-01-07T09:36:53.563709,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:06:53.190970
2026-01-07T09:37:23.960895,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:07:23.620579
2026-01-07T09:37:54.483631,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:07:54.028722
2026-01-07T09:38:24.830000,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:08:24.541359
2026-01-07T09:38:55.172794,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:08:54.897511
2026-01-07T09:39:25.623513,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:09:25.224905
2026-01-07T09:39:55.995074,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:09:55.692820
2026-01-07T09:40:26.348931,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:10:26.051498
2026-01-07T09:40:58.478400,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:10:58.125973
2026-01-07T09:41:28.966832,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:11:28.535212
2026-01-07T09:41:59.333924,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:11:59.041966
2026-01-07T09:42:29.698713,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:12:29.388103
2026-01-07T09:43:00.063643,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:12:59.764519
2026-01-07T09:43:30.406166,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:13:30.120891
2026-01-07T09:44:00.847255,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:14:00.473162
2026-01-07T09:44:31.192335,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:14:30.901099
2026-01-07T09:45:03.278609,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:15:03.006102
2026-01-07T09:45:33.637299,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:15:33.331576
2026-01-07T09:46:05.848385,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:16:05.428889
2026-01-07T09:46:36.210606,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:16:35.900065
2026-01-07T09:47:06.754198,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:17:06.282627
2026-01-07T09:47:37.213471,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:17:36.816809
2026-01-07T09:48:07.582148,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:18:07.287780
2026-01-07T09:48:39.763035,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:18:37.639829
2026-01-07T09:49:10.127931,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:19:09.832361
2026-01-07T09:49:40.547938,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:19:40.190542
2026-01-07T09:50:12.680556,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:20:12.345333
2026-01-07T09:50:43.104788,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:20:42.737711
2026-01-07T09:51:13.588820,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:21:13.174757
2026-01-07T09:51:44.013397,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:21:43.644751
2026-01-07T09:52:14.455854,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:22:14.085635
2026-01-07T09:52:44.928155,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:22:44.509217
2026-01-07T09:53:15.303625,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:23:14.998271
2026-01-07T09:53:45.675126,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:23:45.357485
2026-01-07T09:54:16.052375,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:24:15.741719
2026-01-07T09:54:46.408420,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:24:46.109045
2026-01-07T09:55:16.961849,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:25:16.480650
2026-01-07T09:55:47.344389,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:25:47.025424
2026-01-07T09:56:17.759007,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:26:17.416841
2026-01-07T09:56:48.136219,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:26:47.822932
2026-01-07T09:57:18.610951,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:27:18.205874
2026-01-07T09:57:48.967373,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:27:48.665471
2026-01-07T09:58:19.394248,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:28:19.033762
2026-01-07T09:58:50.761482,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:28:50.460259
2026-01-07T09:59:22.973616,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:29:22.546578
2026-01-07T09:59:53.354210,DEBUG_ENTER_TRY_EXECUTE,2026-01-07T15:29:53.034538
2026-01-08T04:21:16.478860,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T09:51:15.868927
2026-01-08T04:21:16.501765,DEBUG_EXECUTION_DECISION,True,,2026-01-08T09:51:15.868927
2026-01-08T04:21:16.568167,SIP_EXECUTED,12.714881813062679,11.151753419920963,294.92999267578125,112.08999633789062,5000.0
2026-01-08T04:31:47.877937,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:01:47.425004
2026-01-08T04:31:47.881271,DEBUG_EXECUTION_DECISION,False,2026-01-08T09:51:15.868927,2026-01-08T10:01:47.425004
2026-01-08T04:31:47.924244,SIP_EXECUTED,12.714450291445248,11.163704442015103,294.94000244140625,111.97000122070312,5000.0
2026-01-08T04:42:19.203165,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:12:18.648418
2026-01-08T04:42:19.205778,DEBUG_EXECUTION_DECISION,False,2026-01-08T10:01:47.425004,2026-01-08T10:12:18.648418
2026-01-08T04:42:19.331085,SIP_EXECUTED,12.718762876738234,11.158721991243427,294.8399963378906,112.0199966430664,5000.0
2026-01-08T04:52:50.689765,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:22:50.153371
2026-01-08T04:52:50.692344,DEBUG_EXECUTION_DECISION,False,2026-01-08T10:12:18.648418,2026-01-08T10:22:50.153371
2026-01-08T04:53:21.157233,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:23:20.809163
2026-01-08T04:53:51.573210,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:23:51.255571
2026-01-08T04:54:22.308438,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:24:21.655062
2026-01-08T04:54:52.754357,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:24:52.394323
2026-01-08T04:55:23.242880,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:25:22.876929
2026-01-08T04:55:53.680341,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:25:53.334280
2026-01-08T04:56:24.181128,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:26:23.769018
2026-01-08T04:56:54.624604,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:26:54.292088
2026-01-08T04:57:25.085322,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:27:24.707345
2026-01-08T04:57:55.613189,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:27:55.250555
2026-01-08T04:58:26.034367,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:28:25.702622
2026-01-08T04:58:56.555372,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:28:56.119652
2026-01-08T04:59:26.999989,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:29:26.669249
2026-01-08T04:59:57.527945,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:29:57.087547
2026-01-08T05:00:28.120949,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:30:27.650938
2026-01-08T05:00:58.743757,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:30:58.206543
2026-01-08T05:01:29.184022,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:31:28.823369
2026-01-08T05:01:59.687323,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:31:59.273525
2026-01-08T05:02:30.107041,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:32:29.793719
2026-01-08T05:03:00.543088,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:33:00.197591
2026-01-08T05:03:30.965546,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:33:30.635982
2026-01-08T05:04:01.504710,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:34:01.047466
2026-01-08T05:04:31.936220,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:34:31.595331
2026-01-08T05:05:02.505959,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:35:02.022949
2026-01-08T05:05:32.982793,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:35:32.590681
2026-01-08T05:06:03.501447,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:36:03.067249
2026-01-08T05:06:33.905843,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:36:33.595267
2026-01-08T05:07:04.403168,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:37:04.000349
2026-01-08T05:07:34.962315,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:37:34.490966
2026-01-08T05:08:05.530545,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:38:05.048407
2026-01-08T05:08:36.134134,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:38:35.614850
2026-01-08T05:09:06.764438,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:39:06.224561
2026-01-08T05:09:37.407057,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:39:36.849298
2026-01-08T05:10:08.085515,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:40:07.504521
2026-01-08T05:10:38.662254,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:40:38.169149
2026-01-08T05:11:09.243290,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:41:08.755772
2026-01-08T05:11:39.855605,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:41:39.328806
2026-01-08T05:12:10.432899,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:42:09.951667
2026-01-08T05:12:40.975772,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:42:40.515020
2026-01-08T05:13:11.568373,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:43:11.060810
2026-01-08T05:13:42.155839,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:43:41.687103
2026-01-08T05:14:12.747109,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:44:12.243221
2026-01-08T05:14:43.296179,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:44:42.833418
2026-01-08T05:15:13.936010,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:45:13.451789
2026-01-08T05:15:44.485707,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:45:44.022254
2026-01-08T05:16:15.062649,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:46:14.586488
2026-01-08T05:16:45.687717,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:46:45.149839
2026-01-08T05:17:16.268938,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:47:15.773874
2026-01-08T05:17:46.686930,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:47:46.357670
2026-01-08T05:18:17.131710,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:48:16.775661
2026-01-08T05:18:47.585475,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:48:47.244391
2026-01-08T05:19:18.022394,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:49:17.670903
2026-01-08T05:19:48.467310,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:49:48.119012
2026-01-08T05:20:18.944419,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:50:18.558596
2026-01-08T05:20:49.411302,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:50:49.071656
2026-01-08T05:21:19.846297,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:51:19.499991
2026-01-08T05:21:50.508384,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:51:49.973906
2026-01-08T05:22:21.032896,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:52:20.597814
2026-01-08T05:22:51.683189,DEBUG_ENTER_TRY_EXECUTE,2026-01-08T10:52:51.123451

View File

@ -0,0 +1,17 @@
timestamp,nifty_units,gold_units,nifty_price,gold_price,nifty_value,gold_value,portfolio_value,total_invested,pnl
2026-01-10T19:55:25.242070,0.0,0.0,291.1400146484375,113.62999725341797,0.0,0.0,0.0,0.0,0.0
2026-01-10T19:56:26.279661,0.0,0.0,291.1400146484375,113.62999725341797,0.0,0.0,0.0,0.0,0.0
2026-01-10T19:57:33.001987,0.0,0.0,291.1400146484375,113.62999725341797,0.0,0.0,0.0,0.0,0.0
2026-01-10T19:58:34.096752,0.0,0.0,291.1400146484375,113.62999725341797,0.0,0.0,0.0,0.0,0.0
2026-01-10T19:59:35.037266,0.0,0.0,291.1400146484375,113.62999725341797,0.0,0.0,0.0,0.0,0.0
2026-01-10T20:00:36.100378,0.0,0.0,291.1400146484375,113.62999725341797,0.0,0.0,0.0,0.0,0.0
2026-01-10T20:01:37.179304,0.0,0.0,291.1400146484375,113.62999725341797,0.0,0.0,0.0,0.0,0.0
2026-01-10T20:02:39.414103,0.0,0.0,291.1400146484375,113.62999725341797,0.0,0.0,0.0,0.0,0.0
2026-01-10T20:03:40.337199,0.0,0.0,291.1400146484375,113.62999725341797,0.0,0.0,0.0,0.0,0.0
2026-01-10T20:04:41.594502,0.0,0.0,291.1400146484375,113.62999725341797,0.0,0.0,0.0,0.0,0.0
2026-01-10T20:05:42.910014,0.0,0.0,291.1400146484375,113.62999725341797,0.0,0.0,0.0,0.0,0.0
2026-01-10T20:06:44.115662,0.0,0.0,291.1400146484375,113.62999725341797,0.0,0.0,0.0,0.0,0.0
2026-01-10T20:07:45.371083,0.0,0.0,291.1400146484375,113.62999725341797,0.0,0.0,0.0,0.0,0.0
2026-01-10T20:08:46.312195,0.0,0.0,291.1400146484375,113.62999725341797,0.0,0.0,0.0,0.0,0.0
2026-01-10T20:09:47.432052,0.0,0.0,291.1400146484375,113.62999725341797,0.0,0.0,0.0,0.0,0.0
2026-01-10T20:10:48.940888,0.0,0.0,291.1400146484375,113.62999725341797,0.0,0.0,0.0,0.0,0.0

View File

@ -0,0 +1,88 @@
{
"cash": 500000.0,
"positions": {},
"orders": [],
"trades": [],
"equity_curve": [
{
"timestamp": "2026-01-10T19:55:24.730764",
"equity": 500000.0,
"pnl": 0.0
},
{
"timestamp": "2026-01-10T19:56:25.712617",
"equity": 500000.0,
"pnl": 0.0
},
{
"timestamp": "2026-01-10T19:57:32.538338",
"equity": 500000.0,
"pnl": 0.0
},
{
"timestamp": "2026-01-10T19:58:33.531690",
"equity": 500000.0,
"pnl": 0.0
},
{
"timestamp": "2026-01-10T19:59:34.567188",
"equity": 500000.0,
"pnl": 0.0
},
{
"timestamp": "2026-01-10T20:00:35.637880",
"equity": 500000.0,
"pnl": 0.0
},
{
"timestamp": "2026-01-10T20:01:36.742897",
"equity": 500000.0,
"pnl": 0.0
},
{
"timestamp": "2026-01-10T20:02:38.901570",
"equity": 500000.0,
"pnl": 0.0
},
{
"timestamp": "2026-01-10T20:03:39.879929",
"equity": 500000.0,
"pnl": 0.0
},
{
"timestamp": "2026-01-10T20:04:41.074966",
"equity": 500000.0,
"pnl": 0.0
},
{
"timestamp": "2026-01-10T20:05:42.420797",
"equity": 500000.0,
"pnl": 0.0
},
{
"timestamp": "2026-01-10T20:06:43.478764",
"equity": 500000.0,
"pnl": 0.0
},
{
"timestamp": "2026-01-10T20:07:44.680007",
"equity": 500000.0,
"pnl": 0.0
},
{
"timestamp": "2026-01-10T20:08:45.879637",
"equity": 500000.0,
"pnl": 0.0
},
{
"timestamp": "2026-01-10T20:09:46.837127",
"equity": 500000.0,
"pnl": 0.0
},
{
"timestamp": "2026-01-10T20:10:48.256494",
"equity": 500000.0,
"pnl": 0.0
}
]
}

View File

@ -0,0 +1,7 @@
{
"total_invested": 5000.0,
"nifty_units": 12.680485106982822,
"gold_units": 11.335812442673163,
"last_sip_ts": "2026-01-01T11:28:01.549923",
"last_run": "2026-01-01T11:28:01.549923"
}

View File

@ -0,0 +1,13 @@
{
"initial_cash": 500000.0,
"cash": 500000.0,
"total_invested": 0.0,
"nifty_units": 0.0,
"gold_units": 0.0,
"last_sip_ts": null,
"last_run": null,
"sip_frequency": {
"value": 10,
"unit": "minutes"
}
}

View File

@ -0,0 +1,208 @@
import streamlit as st
import time
from datetime import datetime
from pathlib import Path
import sys
import pandas as pd
PROJECT_ROOT = Path(__file__).resolve().parents[2]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from indian_paper_trading_strategy.engine.history import load_monthly_close
from indian_paper_trading_strategy.engine.market import india_market_status
from indian_paper_trading_strategy.engine.data import fetch_live_price
from indian_paper_trading_strategy.engine.strategy import allocation
from indian_paper_trading_strategy.engine.execution import try_execute_sip
from indian_paper_trading_strategy.engine.state import load_state, save_state
from indian_paper_trading_strategy.engine.db import db_connection, insert_engine_event, run_with_retry, get_default_user_id, get_active_run_id, set_context
from indian_paper_trading_strategy.engine.mtm import log_mtm, should_log_mtm
_STREAMLIT_USER_ID = get_default_user_id()
_STREAMLIT_RUN_ID = get_active_run_id(_STREAMLIT_USER_ID) if _STREAMLIT_USER_ID else None
if _STREAMLIT_USER_ID and _STREAMLIT_RUN_ID:
set_context(_STREAMLIT_USER_ID, _STREAMLIT_RUN_ID)
def reset_runtime_state():
def _op(cur, _conn):
cur.execute(
"DELETE FROM mtm_ledger WHERE user_id = %s AND run_id = %s",
(_STREAMLIT_USER_ID, _STREAMLIT_RUN_ID),
)
cur.execute(
"DELETE FROM event_ledger WHERE user_id = %s AND run_id = %s",
(_STREAMLIT_USER_ID, _STREAMLIT_RUN_ID),
)
cur.execute(
"DELETE FROM engine_state WHERE user_id = %s AND run_id = %s",
(_STREAMLIT_USER_ID, _STREAMLIT_RUN_ID),
)
insert_engine_event(cur, "LIVE_RESET", data={})
run_with_retry(_op)
def load_mtm_df():
with db_connection() as conn:
return pd.read_sql_query(
"SELECT timestamp, pnl FROM mtm_ledger WHERE user_id = %s AND run_id = %s ORDER BY timestamp",
conn,
params=(_STREAMLIT_USER_ID, _STREAMLIT_RUN_ID),
)
def is_engine_running():
state = load_state(mode="LIVE")
return state.get("total_invested", 0) > 0 or \
state.get("nifty_units", 0) > 0 or \
state.get("gold_units", 0) > 0
if "engine_active" not in st.session_state:
st.session_state.engine_active = is_engine_running()
NIFTY = "NIFTYBEES.NS"
GOLD = "GOLDBEES.NS"
SMA_MONTHS = 36
def get_prices():
try:
nifty = fetch_live_price(NIFTY)
gold = fetch_live_price(GOLD)
return nifty, gold
except Exception as e:
st.error(e)
return None, None
SIP_AMOUNT = st.number_input("SIP Amount (\u20B9)", 500, 100000, 5000)
SIP_INTERVAL_SEC = st.number_input("SIP Interval (sec) [TEST]", 30, 3600, 120)
REFRESH_SEC = st.slider("Refresh interval (sec)", 5, 60, 10)
st.title("SIPXAR INDIA - Phase-1 Safe Engine")
market_open, market_time = india_market_status()
st.info(f"NSE Market {'OPEN' if market_open else 'CLOSED'} | IST {market_time}")
if not market_open:
st.info("Market is closed. Portfolio values are frozen at last available prices.")
col1, col2 = st.columns(2)
with col1:
if st.button("START ENGINE"):
if is_engine_running():
st.info("Engine already running. Resuming.")
st.session_state.engine_active = True
else:
st.session_state.engine_active = True
# HARD RESET ONLY ON FIRST START
reset_runtime_state()
save_state({
"total_invested": 0.0,
"nifty_units": 0.0,
"gold_units": 0.0,
"last_sip_ts": None,
}, mode="LIVE", emit_event=True, event_meta={"source": "streamlit_start"})
st.success("Engine started")
with col2:
if st.button("KILL ENGINE"):
st.session_state.engine_active = False
reset_runtime_state()
st.warning("Engine killed and state wiped")
st.stop()
if not st.session_state.engine_active:
st.stop()
state = load_state(mode="LIVE")
nifty_price, gold_price = get_prices()
if nifty_price is None:
st.stop()
st.subheader("Latest Market Prices (LTP)")
c1, c2 = st.columns(2)
with c1:
st.metric(
label="NIFTYBEES",
value=f"\u20B9{nifty_price:,.2f}",
help="Last traded price (delayed)"
)
with c2:
st.metric(
label="GOLDBEES",
value=f"\u20B9{gold_price:,.2f}",
help="Last traded price (delayed)"
)
st.caption(f"Price timestamp: {datetime.now().strftime('%H:%M:%S')}")
nifty_hist = load_monthly_close(NIFTY)
gold_hist = load_monthly_close(GOLD)
nifty_sma = nifty_hist.rolling(SMA_MONTHS).mean().iloc[-1]
gold_sma = gold_hist.rolling(SMA_MONTHS).mean().iloc[-1]
eq_w, gd_w = allocation(
sp_price=nifty_price,
gd_price=gold_price,
sp_sma=nifty_sma,
gd_sma=gold_sma
)
state, executed = try_execute_sip(
now=datetime.now(),
market_open=market_open,
sip_interval=SIP_INTERVAL_SEC,
sip_amount=SIP_AMOUNT,
sp_price=nifty_price,
gd_price=gold_price,
eq_w=eq_w,
gd_w=gd_w,
mode="LIVE",
)
now = datetime.now()
if market_open and should_log_mtm(None, now):
portfolio_value, pnl = log_mtm(
nifty_units=state["nifty_units"],
gold_units=state["gold_units"],
nifty_price=nifty_price,
gold_price=gold_price,
total_invested=state["total_invested"],
)
else:
# Market closed -> freeze valuation (do NOT log)
portfolio_value = (
state["nifty_units"] * nifty_price +
state["gold_units"] * gold_price
)
pnl = portfolio_value - state["total_invested"]
st.subheader("Equity Curve (Unrealized PnL)")
mtm_df = load_mtm_df()
if "timestamp" in mtm_df.columns and "pnl" in mtm_df.columns and len(mtm_df) > 1:
mtm_df["timestamp"] = pd.to_datetime(mtm_df["timestamp"])
mtm_df = mtm_df.sort_values("timestamp").set_index("timestamp")
st.line_chart(mtm_df["pnl"], height=350)
else:
st.warning("Not enough MTM data or missing columns. Expected: timestamp, pnl.")
st.metric("Total Invested", f"\u20B9{state['total_invested']:,.0f}")
st.metric("NIFTY Units", round(state["nifty_units"], 4))
st.metric("Gold Units", round(state["gold_units"], 4))
st.metric("Portfolio Value", f"\u20B9{portfolio_value:,.0f}")
st.metric("PnL", f"\u20B9{pnl:,.0f}")
time.sleep(REFRESH_SEC)
st.rerun()

View File

@ -0,0 +1 @@
"""Engine package for the India paper trading strategy."""

View File

@ -0,0 +1,697 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
from datetime import datetime, timezone
import hashlib
from psycopg2.extras import execute_values
from indian_paper_trading_strategy.engine.data import fetch_live_price
from indian_paper_trading_strategy.engine.db import db_connection, insert_engine_event, run_with_retry, get_context
class Broker(ABC):
@abstractmethod
def place_order(
self,
symbol: str,
side: str,
quantity: float,
price: float | None = None,
logical_time: datetime | None = None,
):
raise NotImplementedError
@abstractmethod
def get_positions(self):
raise NotImplementedError
@abstractmethod
def get_orders(self):
raise NotImplementedError
@abstractmethod
def get_funds(self):
raise NotImplementedError
def _local_tz():
return datetime.now().astimezone().tzinfo
def _format_utc_ts(value: datetime | None):
if value is None:
return None
if value.tzinfo is None:
value = value.replace(tzinfo=_local_tz())
return value.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
def _format_local_ts(value: datetime | None):
if value is None:
return None
if value.tzinfo is None:
value = value.replace(tzinfo=_local_tz())
return value.astimezone(_local_tz()).replace(tzinfo=None).isoformat()
def _parse_ts(value, assume_local: bool = True):
if value is None:
return None
if isinstance(value, datetime):
if value.tzinfo is None:
return value.replace(tzinfo=_local_tz() if assume_local else timezone.utc)
return value
if isinstance(value, str):
text = value.strip()
if not text:
return None
if text.endswith("Z"):
try:
return datetime.fromisoformat(text.replace("Z", "+00:00"))
except ValueError:
return None
try:
parsed = datetime.fromisoformat(text)
except ValueError:
return None
if parsed.tzinfo is None:
parsed = parsed.replace(tzinfo=_local_tz() if assume_local else timezone.utc)
return parsed
return None
def _stable_num(value: float) -> str:
return f"{float(value):.12f}"
def _normalize_ts_for_id(ts: datetime) -> str:
if ts.tzinfo is None:
ts = ts.replace(tzinfo=timezone.utc)
return ts.astimezone(timezone.utc).replace(microsecond=0).isoformat()
def _deterministic_id(prefix: str, parts: list[str]) -> str:
payload = "|".join(parts)
digest = hashlib.sha1(payload.encode("utf-8")).hexdigest()[:16]
return f"{prefix}_{digest}"
def _resolve_scope(user_id: str | None, run_id: str | None):
return get_context(user_id, run_id)
@dataclass
class PaperBroker(Broker):
initial_cash: float
store_path: str | None = None
def _default_store(self):
return {
"cash": float(self.initial_cash),
"positions": {},
"orders": [],
"trades": [],
"equity_curve": [],
}
def _load_store(self, cur=None, for_update: bool = False, user_id: str | None = None, run_id: str | None = None):
scope_user, scope_run = _resolve_scope(user_id, run_id)
if cur is None:
with db_connection() as conn:
with conn.cursor() as cur:
return self._load_store(
cur=cur,
for_update=for_update,
user_id=scope_user,
run_id=scope_run,
)
store = self._default_store()
lock_clause = " FOR UPDATE" if for_update else ""
cur.execute(
f"SELECT cash FROM paper_broker_account WHERE user_id = %s AND run_id = %s{lock_clause} LIMIT 1",
(scope_user, scope_run),
)
row = cur.fetchone()
if row and row[0] is not None:
store["cash"] = float(row[0])
cur.execute(
f"""
SELECT symbol, qty, avg_price, last_price
FROM paper_position
WHERE user_id = %s AND run_id = %s{lock_clause}
"""
,
(scope_user, scope_run),
)
positions = {}
for symbol, qty, avg_price, last_price in cur.fetchall():
positions[symbol] = {
"qty": float(qty) if qty is not None else 0.0,
"avg_price": float(avg_price) if avg_price is not None else 0.0,
"last_price": float(last_price) if last_price is not None else 0.0,
}
store["positions"] = positions
cur.execute(
"""
SELECT id, symbol, side, qty, price, status, timestamp, logical_time
FROM paper_order
WHERE user_id = %s AND run_id = %s
ORDER BY timestamp, id
"""
,
(scope_user, scope_run),
)
orders = []
for order_id, symbol, side, qty, price, status, ts, logical_ts in cur.fetchall():
orders.append(
{
"id": order_id,
"symbol": symbol,
"side": side,
"qty": float(qty) if qty is not None else 0.0,
"price": float(price) if price is not None else 0.0,
"status": status,
"timestamp": _format_utc_ts(ts),
"_logical_time": _format_utc_ts(logical_ts),
}
)
store["orders"] = orders
cur.execute(
"""
SELECT id, order_id, symbol, side, qty, price, timestamp, logical_time
FROM paper_trade
WHERE user_id = %s AND run_id = %s
ORDER BY timestamp, id
"""
,
(scope_user, scope_run),
)
trades = []
for trade_id, order_id, symbol, side, qty, price, ts, logical_ts in cur.fetchall():
trades.append(
{
"id": trade_id,
"order_id": order_id,
"symbol": symbol,
"side": side,
"qty": float(qty) if qty is not None else 0.0,
"price": float(price) if price is not None else 0.0,
"timestamp": _format_utc_ts(ts),
"_logical_time": _format_utc_ts(logical_ts),
}
)
store["trades"] = trades
cur.execute(
"""
SELECT timestamp, logical_time, equity, pnl
FROM paper_equity_curve
WHERE user_id = %s AND run_id = %s
ORDER BY timestamp
"""
,
(scope_user, scope_run),
)
equity_curve = []
for ts, logical_ts, equity, pnl in cur.fetchall():
equity_curve.append(
{
"timestamp": _format_local_ts(ts),
"_logical_time": _format_local_ts(logical_ts),
"equity": float(equity) if equity is not None else 0.0,
"pnl": float(pnl) if pnl is not None else 0.0,
}
)
store["equity_curve"] = equity_curve
return store
def _save_store(self, store, cur=None, user_id: str | None = None, run_id: str | None = None):
scope_user, scope_run = _resolve_scope(user_id, run_id)
if cur is None:
def _persist(cur, _conn):
self._save_store(store, cur=cur, user_id=scope_user, run_id=scope_run)
return run_with_retry(_persist)
cash = store.get("cash")
if cash is not None:
cur.execute(
"""
INSERT INTO paper_broker_account (user_id, run_id, cash)
VALUES (%s, %s, %s)
ON CONFLICT (user_id, run_id) DO UPDATE
SET cash = EXCLUDED.cash
""",
(scope_user, scope_run, float(cash)),
)
positions = store.get("positions")
if isinstance(positions, dict):
symbols = [s for s in positions.keys() if s]
if symbols:
cur.execute(
"DELETE FROM paper_position WHERE user_id = %s AND run_id = %s AND symbol NOT IN %s",
(scope_user, scope_run, tuple(symbols)),
)
else:
cur.execute(
"DELETE FROM paper_position WHERE user_id = %s AND run_id = %s",
(scope_user, scope_run),
)
if symbols:
rows = []
updated_at = datetime.now(timezone.utc)
for symbol, data in positions.items():
if not symbol or not isinstance(data, dict):
continue
rows.append(
(
scope_user,
scope_run,
symbol,
float(data.get("qty", 0.0)),
float(data.get("avg_price", 0.0)),
float(data.get("last_price", 0.0)),
updated_at,
)
)
if rows:
execute_values(
cur,
"""
INSERT INTO paper_position (
user_id, run_id, symbol, qty, avg_price, last_price, updated_at
)
VALUES %s
ON CONFLICT (user_id, run_id, symbol) DO UPDATE
SET qty = EXCLUDED.qty,
avg_price = EXCLUDED.avg_price,
last_price = EXCLUDED.last_price,
updated_at = EXCLUDED.updated_at
""",
rows,
)
orders = store.get("orders")
if isinstance(orders, list) and orders:
rows = []
for order in orders:
if not isinstance(order, dict):
continue
order_id = order.get("id")
if not order_id:
continue
ts = _parse_ts(order.get("timestamp"), assume_local=False)
logical_ts = _parse_ts(order.get("_logical_time"), assume_local=False) or ts
rows.append(
(
scope_user,
scope_run,
order_id,
order.get("symbol"),
order.get("side"),
float(order.get("qty", 0.0)),
float(order.get("price", 0.0)),
order.get("status"),
ts,
logical_ts,
)
)
if rows:
execute_values(
cur,
"""
INSERT INTO paper_order (
user_id, run_id, id, symbol, side, qty, price, status, timestamp, logical_time
)
VALUES %s
ON CONFLICT DO NOTHING
""",
rows,
)
trades = store.get("trades")
if isinstance(trades, list) and trades:
rows = []
for trade in trades:
if not isinstance(trade, dict):
continue
trade_id = trade.get("id")
if not trade_id:
continue
ts = _parse_ts(trade.get("timestamp"), assume_local=False)
logical_ts = _parse_ts(trade.get("_logical_time"), assume_local=False) or ts
rows.append(
(
scope_user,
scope_run,
trade_id,
trade.get("order_id"),
trade.get("symbol"),
trade.get("side"),
float(trade.get("qty", 0.0)),
float(trade.get("price", 0.0)),
ts,
logical_ts,
)
)
if rows:
execute_values(
cur,
"""
INSERT INTO paper_trade (
user_id, run_id, id, order_id, symbol, side, qty, price, timestamp, logical_time
)
VALUES %s
ON CONFLICT DO NOTHING
""",
rows,
)
equity_curve = store.get("equity_curve")
if isinstance(equity_curve, list) and equity_curve:
rows = []
for point in equity_curve:
if not isinstance(point, dict):
continue
ts = _parse_ts(point.get("timestamp"), assume_local=True)
logical_ts = _parse_ts(point.get("_logical_time"), assume_local=True) or ts
if ts is None:
continue
rows.append(
(
scope_user,
scope_run,
ts,
logical_ts,
float(point.get("equity", 0.0)),
float(point.get("pnl", 0.0)),
)
)
if rows:
execute_values(
cur,
"""
INSERT INTO paper_equity_curve (user_id, run_id, timestamp, logical_time, equity, pnl)
VALUES %s
ON CONFLICT DO NOTHING
""",
rows,
)
def get_funds(self, cur=None):
store = self._load_store(cur=cur)
cash = float(store.get("cash", 0))
positions = store.get("positions", {})
positions_value = 0.0
for position in positions.values():
qty = float(position.get("qty", 0))
last_price = float(position.get("last_price", position.get("avg_price", 0)))
positions_value += qty * last_price
total_equity = cash + positions_value
return {
"cash_available": cash,
"invested_value": positions_value,
"cash": cash,
"used_margin": 0.0,
"available": cash,
"net": total_equity,
"total_equity": total_equity,
}
def get_positions(self, cur=None):
store = self._load_store(cur=cur)
positions = store.get("positions", {})
return [
{
"symbol": symbol,
"qty": float(data.get("qty", 0)),
"avg_price": float(data.get("avg_price", 0)),
"last_price": float(data.get("last_price", data.get("avg_price", 0))),
}
for symbol, data in positions.items()
]
def get_orders(self, cur=None):
store = self._load_store(cur=cur)
orders = []
for order in store.get("orders", []):
if isinstance(order, dict):
order = {k: v for k, v in order.items() if k != "_logical_time"}
orders.append(order)
return orders
def get_trades(self, cur=None):
store = self._load_store(cur=cur)
trades = []
for trade in store.get("trades", []):
if isinstance(trade, dict):
trade = {k: v for k, v in trade.items() if k != "_logical_time"}
trades.append(trade)
return trades
def get_equity_curve(self, cur=None):
store = self._load_store(cur=cur)
points = []
for point in store.get("equity_curve", []):
if isinstance(point, dict):
point = {k: v for k, v in point.items() if k != "_logical_time"}
points.append(point)
return points
def _update_equity_in_tx(
self,
cur,
prices: dict[str, float],
now: datetime,
logical_time: datetime | None = None,
user_id: str | None = None,
run_id: str | None = None,
):
store = self._load_store(cur=cur, for_update=True, user_id=user_id, run_id=run_id)
positions = store.get("positions", {})
for symbol, price in prices.items():
if symbol in positions:
positions[symbol]["last_price"] = float(price)
cash = float(store.get("cash", 0))
positions_value = 0.0
for symbol, position in positions.items():
qty = float(position.get("qty", 0))
price = float(position.get("last_price", position.get("avg_price", 0)))
positions_value += qty * price
equity = cash + positions_value
pnl = equity - float(self.initial_cash)
ts_for_equity = logical_time or now
store.setdefault("equity_curve", []).append(
{
"timestamp": _format_local_ts(ts_for_equity),
"_logical_time": _format_local_ts(ts_for_equity),
"equity": equity,
"pnl": pnl,
}
)
store["positions"] = positions
self._save_store(store, cur=cur, user_id=user_id, run_id=run_id)
insert_engine_event(
cur,
"EQUITY_UPDATED",
data={
"timestamp": _format_utc_ts(ts_for_equity),
"equity": equity,
"pnl": pnl,
},
)
return equity
def update_equity(
self,
prices: dict[str, float],
now: datetime,
cur=None,
logical_time: datetime | None = None,
user_id: str | None = None,
run_id: str | None = None,
):
if cur is not None:
return self._update_equity_in_tx(
cur,
prices,
now,
logical_time=logical_time,
user_id=user_id,
run_id=run_id,
)
def _op(cur, _conn):
return self._update_equity_in_tx(
cur,
prices,
now,
logical_time=logical_time,
user_id=user_id,
run_id=run_id,
)
return run_with_retry(_op)
def _place_order_in_tx(
self,
cur,
symbol: str,
side: str,
quantity: float,
price: float | None,
logical_time: datetime | None = None,
user_id: str | None = None,
run_id: str | None = None,
):
scope_user, scope_run = _resolve_scope(user_id, run_id)
store = self._load_store(cur=cur, for_update=True, user_id=scope_user, run_id=scope_run)
side = side.upper().strip()
qty = float(quantity)
if price is None:
price = fetch_live_price(symbol)
price = float(price)
logical_ts = logical_time or datetime.utcnow().replace(tzinfo=timezone.utc)
timestamp = logical_ts
timestamp_str = _format_utc_ts(timestamp)
logical_ts_str = _format_utc_ts(logical_ts)
order_id = _deterministic_id(
"ord",
[
scope_user,
scope_run,
_normalize_ts_for_id(logical_ts),
symbol,
side,
_stable_num(qty),
_stable_num(price),
],
)
order = {
"id": order_id,
"symbol": symbol,
"side": side,
"qty": qty,
"price": price,
"status": "REJECTED",
"timestamp": timestamp_str,
"_logical_time": logical_ts_str,
}
if qty <= 0 or price <= 0:
store.setdefault("orders", []).append(order)
self._save_store(store, cur=cur, user_id=user_id, run_id=run_id)
insert_engine_event(cur, "ORDER_PLACED", data=order)
return order
positions = store.get("positions", {})
cash = float(store.get("cash", 0))
trade = None
if side == "BUY":
cost = qty * price
if cash >= cost:
cash -= cost
existing = positions.get(symbol, {"qty": 0.0, "avg_price": 0.0, "last_price": price})
new_qty = float(existing.get("qty", 0)) + qty
prev_cost = float(existing.get("qty", 0)) * float(existing.get("avg_price", 0))
avg_price = (prev_cost + cost) / new_qty if new_qty else price
positions[symbol] = {
"qty": new_qty,
"avg_price": avg_price,
"last_price": price,
}
order["status"] = "FILLED"
trade = {
"id": _deterministic_id("trd", [order_id]),
"order_id": order_id,
"symbol": symbol,
"side": side,
"qty": qty,
"price": price,
"timestamp": timestamp_str,
"_logical_time": logical_ts_str,
}
store.setdefault("trades", []).append(trade)
elif side == "SELL":
existing = positions.get(symbol)
if existing and float(existing.get("qty", 0)) >= qty:
cash += qty * price
remaining = float(existing.get("qty", 0)) - qty
if remaining > 0:
existing["qty"] = remaining
existing["last_price"] = price
positions[symbol] = existing
else:
positions.pop(symbol, None)
order["status"] = "FILLED"
trade = {
"id": _deterministic_id("trd", [order_id]),
"order_id": order_id,
"symbol": symbol,
"side": side,
"qty": qty,
"price": price,
"timestamp": timestamp_str,
"_logical_time": logical_ts_str,
}
store.setdefault("trades", []).append(trade)
store["cash"] = cash
store["positions"] = positions
store.setdefault("orders", []).append(order)
self._save_store(store, cur=cur, user_id=user_id, run_id=run_id)
insert_engine_event(cur, "ORDER_PLACED", data=order)
if trade is not None:
insert_engine_event(cur, "TRADE_EXECUTED", data=trade)
insert_engine_event(cur, "ORDER_FILLED", data={"order_id": order_id})
return order
def place_order(
self,
symbol: str,
side: str,
quantity: float,
price: float | None = None,
cur=None,
logical_time: datetime | None = None,
user_id: str | None = None,
run_id: str | None = None,
):
if cur is not None:
return self._place_order_in_tx(
cur,
symbol,
side,
quantity,
price,
logical_time=logical_time,
user_id=user_id,
run_id=run_id,
)
def _op(cur, _conn):
return self._place_order_in_tx(
cur,
symbol,
side,
quantity,
price,
logical_time=logical_time,
user_id=user_id,
run_id=run_id,
)
return run_with_retry(_op)

View File

@ -0,0 +1,150 @@
import json
from datetime import datetime
from indian_paper_trading_strategy.engine.db import db_connection, get_context
DEFAULT_CONFIG = {
"active": False,
"sip_amount": 0,
"sip_frequency": {"value": 30, "unit": "days"},
"next_run": None
}
def _maybe_parse_json(value):
if value is None:
return None
if not isinstance(value, str):
return value
text = value.strip()
if not text:
return None
try:
return json.loads(text)
except Exception:
return value
def _format_ts(value: datetime | None):
if value is None:
return None
return value.isoformat()
def load_strategy_config(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 strategy, sip_amount, sip_frequency_value, sip_frequency_unit,
mode, broker, active, frequency, frequency_days, unit, next_run
FROM strategy_config
WHERE user_id = %s AND run_id = %s
LIMIT 1
""",
(scope_user, scope_run),
)
row = cur.fetchone()
if not row:
return DEFAULT_CONFIG.copy()
cfg = DEFAULT_CONFIG.copy()
cfg["strategy"] = row[0]
cfg["strategy_name"] = row[0]
cfg["sip_amount"] = float(row[1]) if row[1] is not None else cfg.get("sip_amount")
cfg["mode"] = row[4]
cfg["broker"] = row[5]
cfg["active"] = row[6] if row[6] is not None else cfg.get("active")
cfg["frequency"] = _maybe_parse_json(row[7])
cfg["frequency_days"] = row[8]
cfg["unit"] = row[9]
cfg["next_run"] = _format_ts(row[10])
if row[2] is not None or row[3] is not None:
cfg["sip_frequency"] = {"value": row[2], "unit": row[3]}
else:
value = cfg.get("frequency")
unit = cfg.get("unit")
if isinstance(value, dict):
unit = value.get("unit", unit)
value = value.get("value")
if value is None and cfg.get("frequency_days") is not None:
value = cfg.get("frequency_days")
unit = unit or "days"
if value is not None and unit:
cfg["sip_frequency"] = {"value": value, "unit": unit}
return cfg
def save_strategy_config(cfg, user_id: str | None = None, run_id: str | None = None):
scope_user, scope_run = get_context(user_id, run_id)
sip_frequency = cfg.get("sip_frequency")
sip_value = None
sip_unit = None
if isinstance(sip_frequency, dict):
sip_value = sip_frequency.get("value")
sip_unit = sip_frequency.get("unit")
frequency = cfg.get("frequency")
if not isinstance(frequency, str) and frequency is not None:
frequency = json.dumps(frequency)
next_run = cfg.get("next_run")
next_run_dt = None
if isinstance(next_run, str):
try:
next_run_dt = datetime.fromisoformat(next_run)
except ValueError:
next_run_dt = None
strategy = cfg.get("strategy") or cfg.get("strategy_name")
with db_connection() as conn:
with conn:
with conn.cursor() as cur:
cur.execute(
"""
INSERT INTO strategy_config (
user_id,
run_id,
strategy,
sip_amount,
sip_frequency_value,
sip_frequency_unit,
mode,
broker,
active,
frequency,
frequency_days,
unit,
next_run
)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (user_id, run_id) DO UPDATE
SET strategy = EXCLUDED.strategy,
sip_amount = EXCLUDED.sip_amount,
sip_frequency_value = EXCLUDED.sip_frequency_value,
sip_frequency_unit = EXCLUDED.sip_frequency_unit,
mode = EXCLUDED.mode,
broker = EXCLUDED.broker,
active = EXCLUDED.active,
frequency = EXCLUDED.frequency,
frequency_days = EXCLUDED.frequency_days,
unit = EXCLUDED.unit,
next_run = EXCLUDED.next_run
""",
(
scope_user,
scope_run,
strategy,
cfg.get("sip_amount"),
sip_value,
sip_unit,
cfg.get("mode"),
cfg.get("broker"),
cfg.get("active"),
frequency,
cfg.get("frequency_days"),
cfg.get("unit"),
next_run_dt,
),
)

View File

@ -0,0 +1,81 @@
# engine/data.py
from datetime import datetime, timezone
from pathlib import Path
import os
import threading
import pandas as pd
import yfinance as yf
ENGINE_ROOT = Path(__file__).resolve().parents[1]
HISTORY_DIR = ENGINE_ROOT / "storage" / "history"
ALLOW_PRICE_CACHE = os.getenv("ALLOW_PRICE_CACHE", "0").strip().lower() in {"1", "true", "yes"}
_LAST_PRICE: dict[str, dict[str, object]] = {}
_LAST_PRICE_LOCK = threading.Lock()
def _set_last_price(ticker: str, price: float, source: str):
now = datetime.now(timezone.utc)
with _LAST_PRICE_LOCK:
_LAST_PRICE[ticker] = {"price": float(price), "source": source, "ts": now}
def get_price_snapshot(ticker: str) -> dict[str, object] | None:
with _LAST_PRICE_LOCK:
data = _LAST_PRICE.get(ticker)
if not data:
return None
return dict(data)
def _get_last_live_price(ticker: str) -> float | None:
with _LAST_PRICE_LOCK:
data = _LAST_PRICE.get(ticker)
if not data:
return None
if data.get("source") == "live":
return float(data.get("price", 0))
return None
def _cached_last_close(ticker: str) -> float | None:
file = HISTORY_DIR / f"{ticker}.csv"
if not file.exists():
return None
df = pd.read_csv(file)
if df.empty or "Close" not in df.columns:
return None
return float(df["Close"].iloc[-1])
def fetch_live_price(ticker, allow_cache: bool | None = None):
if allow_cache is None:
allow_cache = ALLOW_PRICE_CACHE
try:
df = yf.download(
ticker,
period="1d",
interval="1m",
auto_adjust=True,
progress=False,
timeout=5,
)
if df is not None and not df.empty:
price = float(df["Close"].iloc[-1])
_set_last_price(ticker, price, "live")
return price
except Exception:
pass
if allow_cache:
last_live = _get_last_live_price(ticker)
if last_live is not None:
return last_live
cached = _cached_last_close(ticker)
if cached is not None:
_set_last_price(ticker, cached, "cache")
return cached
raise RuntimeError(f"No live data for {ticker}")

View File

@ -0,0 +1,198 @@
import time
from datetime import datetime, timezone
from indian_paper_trading_strategy.engine.db import (
run_with_retry,
insert_engine_event,
get_default_user_id,
get_active_run_id,
get_running_runs,
engine_context,
)
def log_event(event: str, data: dict | None = None):
now = datetime.utcnow().replace(tzinfo=timezone.utc)
payload = data or {}
def _op(cur, _conn):
insert_engine_event(cur, event, data=payload, ts=now)
run_with_retry(_op)
def _update_engine_status(user_id: str, run_id: str, status: str):
now = datetime.utcnow().replace(tzinfo=timezone.utc)
def _op(cur, _conn):
cur.execute(
"""
INSERT INTO engine_status (user_id, run_id, status, last_updated)
VALUES (%s, %s, %s, %s)
ON CONFLICT (user_id, run_id) DO UPDATE
SET status = EXCLUDED.status,
last_updated = EXCLUDED.last_updated
""",
(user_id, run_id, status, now),
)
run_with_retry(_op)
from indian_paper_trading_strategy.engine.config import load_strategy_config, save_strategy_config
from indian_paper_trading_strategy.engine.market import india_market_status
from indian_paper_trading_strategy.engine.execution import try_execute_sip
from indian_paper_trading_strategy.engine.state import load_state
from indian_paper_trading_strategy.engine.broker import PaperBroker
from indian_paper_trading_strategy.engine.data import fetch_live_price
from indian_paper_trading_strategy.engine.mtm import log_mtm, should_log_mtm
from indian_paper_trading_strategy.engine.history import load_monthly_close
from indian_paper_trading_strategy.engine.strategy import allocation
from indian_paper_trading_strategy.engine.time_utils import frequency_to_timedelta, normalize_logical_time
NIFTY = "NIFTYBEES.NS"
GOLD = "GOLDBEES.NS"
SMA_MONTHS = 36
def run_engine(user_id: str | None = None, run_id: str | None = None):
print("Strategy engine started")
active_runs: dict[tuple[str, str], bool] = {}
if run_id and not user_id:
raise ValueError("user_id is required when run_id is provided")
while True:
try:
if user_id and run_id:
runs = [(user_id, run_id)]
elif user_id:
runs = get_running_runs(user_id)
else:
runs = get_running_runs()
if not runs:
default_user = get_default_user_id()
if default_user:
runs = get_running_runs(default_user)
seen = set()
for scope_user, scope_run in runs:
if not scope_user or not scope_run:
continue
seen.add((scope_user, scope_run))
with engine_context(scope_user, scope_run):
cfg = load_strategy_config(user_id=scope_user, run_id=scope_run)
if not cfg.get("active"):
continue
strategy_name = cfg.get("strategy_name", "golden_nifty")
sip_amount = cfg.get("sip_amount", 0)
configured_frequency = cfg.get("sip_frequency") or {}
if not isinstance(configured_frequency, dict):
configured_frequency = {}
frequency_value = int(configured_frequency.get("value", cfg.get("frequency", 0)))
frequency_unit = configured_frequency.get("unit", cfg.get("unit", "days"))
frequency_info = {"value": frequency_value, "unit": frequency_unit}
frequency_label = f"{frequency_value} {frequency_unit}"
if not active_runs.get((scope_user, scope_run)):
log_event(
"ENGINE_START",
{
"strategy": strategy_name,
"sip_amount": sip_amount,
"frequency": frequency_label,
},
)
active_runs[(scope_user, scope_run)] = True
_update_engine_status(scope_user, scope_run, "RUNNING")
market_open, _ = india_market_status()
if not market_open:
log_event("MARKET_CLOSED", {"reason": "Outside market hours"})
continue
now = datetime.now()
mode = (cfg.get("mode") or "PAPER").strip().upper()
if mode not in {"PAPER", "LIVE"}:
mode = "PAPER"
state = load_state(mode=mode)
initial_cash = float(state.get("initial_cash") or 0.0)
broker = PaperBroker(initial_cash=initial_cash) if mode == "PAPER" else None
nifty_price = fetch_live_price(NIFTY)
gold_price = fetch_live_price(GOLD)
next_run = cfg.get("next_run")
if next_run is None or now >= datetime.fromisoformat(next_run):
nifty_hist = load_monthly_close(NIFTY)
gold_hist = load_monthly_close(GOLD)
nifty_sma = nifty_hist.rolling(SMA_MONTHS).mean().iloc[-1]
gold_sma = gold_hist.rolling(SMA_MONTHS).mean().iloc[-1]
eq_w, gd_w = allocation(
sp_price=nifty_price,
gd_price=gold_price,
sp_sma=nifty_sma,
gd_sma=gold_sma,
)
weights = {"equity": eq_w, "gold": gd_w}
state, executed = try_execute_sip(
now=now,
market_open=True,
sip_interval=frequency_to_timedelta(frequency_info).total_seconds(),
sip_amount=sip_amount,
sp_price=nifty_price,
gd_price=gold_price,
eq_w=eq_w,
gd_w=gd_w,
broker=broker,
mode=mode,
)
if executed:
log_event(
"SIP_TRIGGERED",
{
"date": now.date().isoformat(),
"allocation": weights,
"cash_used": sip_amount,
},
)
portfolio_value = (
state["nifty_units"] * nifty_price
+ state["gold_units"] * gold_price
)
log_event(
"PORTFOLIO_UPDATED",
{
"nifty_units": state["nifty_units"],
"gold_units": state["gold_units"],
"portfolio_value": portfolio_value,
},
)
cfg["next_run"] = (now + frequency_to_timedelta(frequency_info)).isoformat()
save_strategy_config(cfg, user_id=scope_user, run_id=scope_run)
if should_log_mtm(None, now):
state = load_state(mode=mode)
log_mtm(
nifty_units=state["nifty_units"],
gold_units=state["gold_units"],
nifty_price=nifty_price,
gold_price=gold_price,
total_invested=state["total_invested"],
logical_time=normalize_logical_time(now),
)
for key in list(active_runs.keys()):
if key not in seen:
active_runs.pop(key, None)
time.sleep(30)
except Exception as e:
log_event("ENGINE_ERROR", {"error": str(e)})
raise
if __name__ == "__main__":
run_engine()

View File

@ -0,0 +1,157 @@
# 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)

View File

@ -0,0 +1,34 @@
# engine/history.py
import yfinance as yf
import pandas as pd
from pathlib import Path
ENGINE_ROOT = Path(__file__).resolve().parents[1]
STORAGE_DIR = ENGINE_ROOT / "storage"
STORAGE_DIR.mkdir(exist_ok=True)
CACHE_DIR = STORAGE_DIR / "history"
CACHE_DIR.mkdir(exist_ok=True)
def load_monthly_close(ticker, years=10):
file = CACHE_DIR / f"{ticker}.csv"
if file.exists():
df = pd.read_csv(file, parse_dates=["Date"], index_col="Date")
return df["Close"]
df = yf.download(
ticker,
period=f"{years}y",
auto_adjust=True,
progress=False,
timeout=5,
)
if df.empty:
raise RuntimeError(f"No history for {ticker}")
series = df["Close"].resample("M").last()
series.to_csv(file, header=["Close"])
return series

View File

@ -0,0 +1,113 @@
# engine/ledger.py
from datetime import datetime, timezone
from indian_paper_trading_strategy.engine.db import insert_engine_event, run_with_retry, get_context
from indian_paper_trading_strategy.engine.time_utils import normalize_logical_time
def _event_exists_in_tx(cur, event, logical_time, 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)
cur.execute(
"""
SELECT 1
FROM event_ledger
WHERE user_id = %s AND run_id = %s AND event = %s AND logical_time = %s
LIMIT 1
""",
(scope_user, scope_run, event, logical_ts),
)
return cur.fetchone() is not None
def event_exists(event, logical_time, *, cur=None, user_id: str | None = None, run_id: str | None = None):
if cur is not None:
return _event_exists_in_tx(cur, event, logical_time, user_id=user_id, run_id=run_id)
def _op(cur, _conn):
return _event_exists_in_tx(cur, event, logical_time, user_id=user_id, run_id=run_id)
return run_with_retry(_op)
def _log_event_in_tx(
cur,
event,
payload,
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)
cur.execute(
"""
INSERT INTO event_ledger (
user_id,
run_id,
timestamp,
logical_time,
event,
nifty_units,
gold_units,
nifty_price,
gold_price,
amount
)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
ON CONFLICT DO NOTHING
""",
(
scope_user,
scope_run,
ts,
logical_ts,
event,
payload.get("nifty_units"),
payload.get("gold_units"),
payload.get("nifty_price"),
payload.get("gold_price"),
payload.get("amount"),
),
)
if cur.rowcount:
insert_engine_event(cur, event, data=payload, ts=ts)
def log_event(
event,
payload,
*,
cur=None,
ts=None,
logical_time=None,
user_id: str | None = None,
run_id: str | None = None,
):
now = ts or logical_time or datetime.utcnow().replace(tzinfo=timezone.utc)
if cur is not None:
_log_event_in_tx(
cur,
event,
payload,
now,
logical_time=logical_time,
user_id=user_id,
run_id=run_id,
)
return
def _op(cur, _conn):
_log_event_in_tx(
cur,
event,
payload,
now,
logical_time=logical_time,
user_id=user_id,
run_id=run_id,
)
return run_with_retry(_op)

View File

@ -0,0 +1,42 @@
# engine/market.py
from datetime import datetime, time as dtime, timedelta
import pytz
_MARKET_TZ = pytz.timezone("Asia/Kolkata")
_OPEN_T = dtime(9, 15)
_CLOSE_T = dtime(15, 30)
def _as_market_tz(value: datetime) -> datetime:
if value.tzinfo is None:
return _MARKET_TZ.localize(value)
return value.astimezone(_MARKET_TZ)
def is_market_open(now: datetime) -> bool:
now = _as_market_tz(now)
return now.weekday() < 5 and _OPEN_T <= now.time() <= _CLOSE_T
def india_market_status():
now = datetime.now(_MARKET_TZ)
return is_market_open(now), now
def next_market_open_after(value: datetime) -> datetime:
current = _as_market_tz(value)
while current.weekday() >= 5:
current = current + timedelta(days=1)
current = current.replace(hour=_OPEN_T.hour, minute=_OPEN_T.minute, second=0, microsecond=0)
if current.time() < _OPEN_T:
return current.replace(hour=_OPEN_T.hour, minute=_OPEN_T.minute, second=0, microsecond=0)
if current.time() > _CLOSE_T:
current = current + timedelta(days=1)
while current.weekday() >= 5:
current = current + timedelta(days=1)
return current.replace(hour=_OPEN_T.hour, minute=_OPEN_T.minute, second=0, microsecond=0)
return current
def align_to_market_open(value: datetime) -> datetime:
current = _as_market_tz(value)
aligned = current if is_market_open(current) else next_market_open_after(current)
if value.tzinfo is None:
return aligned.replace(tzinfo=None)
return aligned

View File

@ -0,0 +1,154 @@
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

View File

@ -0,0 +1,518 @@
import os
import threading
import time
from datetime import datetime, timedelta, timezone
from indian_paper_trading_strategy.engine.market import is_market_open, align_to_market_open
from indian_paper_trading_strategy.engine.execution import try_execute_sip
from indian_paper_trading_strategy.engine.broker import PaperBroker
from indian_paper_trading_strategy.engine.mtm import log_mtm, should_log_mtm
from indian_paper_trading_strategy.engine.state import load_state
from indian_paper_trading_strategy.engine.data import fetch_live_price
from indian_paper_trading_strategy.engine.history import load_monthly_close
from indian_paper_trading_strategy.engine.strategy import allocation
from indian_paper_trading_strategy.engine.time_utils import normalize_logical_time
from indian_paper_trading_strategy.engine.db import db_transaction, insert_engine_event, run_with_retry, get_context, set_context
def _update_engine_status(user_id: str, run_id: str, status: str):
now = datetime.utcnow().replace(tzinfo=timezone.utc)
def _op(cur, _conn):
cur.execute(
"""
INSERT INTO engine_status (user_id, run_id, status, last_updated)
VALUES (%s, %s, %s, %s)
ON CONFLICT (user_id, run_id) DO UPDATE
SET status = EXCLUDED.status,
last_updated = EXCLUDED.last_updated
""",
(user_id, run_id, status, now),
)
run_with_retry(_op)
NIFTY = "NIFTYBEES.NS"
GOLD = "GOLDBEES.NS"
SMA_MONTHS = 36
_DEFAULT_ENGINE_STATE = {
"state": "STOPPED",
"run_id": None,
"user_id": None,
"last_heartbeat_ts": None,
}
_ENGINE_STATES = {}
_ENGINE_STATES_LOCK = threading.Lock()
_RUNNERS = {}
_RUNNERS_LOCK = threading.Lock()
engine_state = _ENGINE_STATES
def _state_key(user_id: str, run_id: str):
return (user_id, run_id)
def _get_state(user_id: str, run_id: str):
key = _state_key(user_id, run_id)
with _ENGINE_STATES_LOCK:
state = _ENGINE_STATES.get(key)
if state is None:
state = dict(_DEFAULT_ENGINE_STATE)
state["user_id"] = user_id
state["run_id"] = run_id
_ENGINE_STATES[key] = state
return state
def _set_state(user_id: str, run_id: str, **updates):
key = _state_key(user_id, run_id)
with _ENGINE_STATES_LOCK:
state = _ENGINE_STATES.get(key)
if state is None:
state = dict(_DEFAULT_ENGINE_STATE)
state["user_id"] = user_id
state["run_id"] = run_id
_ENGINE_STATES[key] = state
state.update(updates)
def get_engine_state(user_id: str, run_id: str):
state = _get_state(user_id, run_id)
return dict(state)
def log_event(
event: str,
data: dict | None = None,
message: str | None = None,
meta: dict | None = None,
):
entry = {
"ts": datetime.utcnow().replace(tzinfo=timezone.utc).isoformat(),
"event": event,
}
if message is not None or meta is not None:
entry["message"] = message or ""
entry["meta"] = meta or {}
else:
entry["data"] = data or {}
event_ts = datetime.fromisoformat(entry["ts"].replace("Z", "+00:00"))
data = entry.get("data") if "data" in entry else None
meta = entry.get("meta") if "meta" in entry else None
def _op(cur, _conn):
insert_engine_event(
cur,
entry.get("event"),
data=data,
message=entry.get("message"),
meta=meta,
ts=event_ts,
)
run_with_retry(_op)
def sleep_with_heartbeat(
total_seconds: int,
stop_event: threading.Event,
user_id: str,
run_id: str,
step_seconds: int = 5,
):
remaining = total_seconds
while remaining > 0 and not stop_event.is_set():
time.sleep(min(step_seconds, remaining))
_set_state(user_id, run_id, last_heartbeat_ts=datetime.utcnow().isoformat() + "Z")
remaining -= step_seconds
def _clear_runner(user_id: str, run_id: str):
key = _state_key(user_id, run_id)
with _RUNNERS_LOCK:
_RUNNERS.pop(key, None)
def can_execute(now: datetime) -> tuple[bool, str]:
if not is_market_open(now):
return False, "MARKET_CLOSED"
return True, "OK"
def _engine_loop(config, stop_event: threading.Event):
print("Strategy engine started with config:", config)
user_id = config.get("user_id")
run_id = config.get("run_id")
scope_user, scope_run = get_context(user_id, run_id)
set_context(scope_user, scope_run)
strategy_name = config.get("strategy_name") or config.get("strategy") or "golden_nifty"
sip_amount = config["sip_amount"]
configured_frequency = config.get("sip_frequency") or {}
if not isinstance(configured_frequency, dict):
configured_frequency = {}
frequency_value = int(configured_frequency.get("value", config.get("frequency", 0)))
frequency_unit = configured_frequency.get("unit", config.get("unit", "days"))
frequency_label = f"{frequency_value} {frequency_unit}"
emit_event_cb = config.get("emit_event")
if not callable(emit_event_cb):
emit_event_cb = None
debug_enabled = os.getenv("ENGINE_DEBUG", "1").strip().lower() not in {"0", "false", "no"}
def debug_event(event: str, message: str, meta: dict | None = None):
if not debug_enabled:
return
try:
log_event(event=event, message=message, meta=meta or {})
except Exception:
pass
if emit_event_cb:
emit_event_cb(event=event, message=message, meta=meta or {})
print(f"[ENGINE] {event} {message} {meta or {}}", flush=True)
mode = (config.get("mode") or "LIVE").strip().upper()
if mode not in {"PAPER", "LIVE"}:
mode = "LIVE"
broker_type = config.get("broker") or "paper"
if broker_type != "paper":
broker_type = "paper"
if broker_type == "paper":
mode = "PAPER"
initial_cash = float(config.get("initial_cash", 0))
broker = PaperBroker(initial_cash=initial_cash)
log_event(
event="DEBUG_PAPER_STORE_PATH",
message="Paper broker store path",
meta={
"cwd": os.getcwd(),
"paper_store_path": str(broker.store_path) if hasattr(broker, "store_path") else "NO_STORE_PATH",
"abs_store_path": os.path.abspath(str(broker.store_path)) if hasattr(broker, "store_path") else "N/A",
},
)
if emit_event_cb:
emit_event_cb(
event="DEBUG_PAPER_STORE_PATH",
message="Paper broker store path",
meta={
"cwd": os.getcwd(),
"paper_store_path": str(broker.store_path) if hasattr(broker, "store_path") else "NO_STORE_PATH",
"abs_store_path": os.path.abspath(str(broker.store_path)) if hasattr(broker, "store_path") else "N/A",
},
)
log_event("ENGINE_START", {
"strategy": strategy_name,
"sip_amount": sip_amount,
"frequency": frequency_label,
})
debug_event("ENGINE_START_DEBUG", "engine loop started", {"run_id": scope_run, "user_id": scope_user})
_set_state(
scope_user,
scope_run,
state="RUNNING",
last_heartbeat_ts=datetime.utcnow().isoformat() + "Z",
)
_update_engine_status(scope_user, scope_run, "RUNNING")
try:
while not stop_event.is_set():
_set_state(scope_user, scope_run, last_heartbeat_ts=datetime.utcnow().isoformat() + "Z")
_update_engine_status(scope_user, scope_run, "RUNNING")
state = load_state(mode=mode)
debug_event(
"STATE_LOADED",
"loaded engine state",
{
"last_sip_ts": state.get("last_sip_ts"),
"last_run": state.get("last_run"),
"cash": state.get("cash"),
"total_invested": state.get("total_invested"),
},
)
state_frequency = state.get("sip_frequency")
if not isinstance(state_frequency, dict):
state_frequency = {"value": frequency_value, "unit": frequency_unit}
freq = int(state_frequency.get("value", frequency_value))
unit = state_frequency.get("unit", frequency_unit)
frequency_label = f"{freq} {unit}"
if unit == "minutes":
delta = timedelta(minutes=freq)
else:
delta = timedelta(days=freq)
# Gate 2: time to SIP
last_run = state.get("last_run") or state.get("last_sip_ts")
is_first_run = last_run is None
now = datetime.now()
debug_event(
"ENGINE_LOOP_TICK",
"engine loop tick",
{"now": now.isoformat(), "frequency": frequency_label},
)
if last_run and not is_first_run:
next_run = datetime.fromisoformat(last_run) + delta
next_run = align_to_market_open(next_run)
if now < next_run:
log_event(
event="SIP_WAITING",
message="Waiting for next SIP window",
meta={
"last_run": last_run,
"next_eligible": next_run.isoformat(),
"now": now.isoformat(),
"frequency": frequency_label,
},
)
if emit_event_cb:
emit_event_cb(
event="SIP_WAITING",
message="Waiting for next SIP window",
meta={
"last_run": last_run,
"next_eligible": next_run.isoformat(),
"now": now.isoformat(),
"frequency": frequency_label,
},
)
sleep_with_heartbeat(60, stop_event, scope_user, scope_run)
continue
try:
debug_event("PRICE_FETCH_START", "fetching live prices", {"tickers": [NIFTY, GOLD]})
nifty_price = fetch_live_price(NIFTY)
gold_price = fetch_live_price(GOLD)
debug_event(
"PRICE_FETCHED",
"fetched live prices",
{"nifty_price": float(nifty_price), "gold_price": float(gold_price)},
)
except Exception as exc:
debug_event("PRICE_FETCH_ERROR", "live price fetch failed", {"error": str(exc)})
sleep_with_heartbeat(30, stop_event, scope_user, scope_run)
continue
try:
nifty_hist = load_monthly_close(NIFTY)
gold_hist = load_monthly_close(GOLD)
except Exception as exc:
debug_event("HISTORY_LOAD_ERROR", "history load failed", {"error": str(exc)})
sleep_with_heartbeat(30, stop_event, scope_user, scope_run)
continue
nifty_sma = nifty_hist.rolling(SMA_MONTHS).mean().iloc[-1]
gold_sma = gold_hist.rolling(SMA_MONTHS).mean().iloc[-1]
eq_w, gd_w = allocation(
sp_price=nifty_price,
gd_price=gold_price,
sp_sma=nifty_sma,
gd_sma=gold_sma
)
debug_event(
"WEIGHTS_COMPUTED",
"computed allocation weights",
{"equity_weight": float(eq_w), "gold_weight": float(gd_w)},
)
weights = {"equity": eq_w, "gold": gd_w}
allowed, reason = can_execute(now)
executed = False
if not allowed:
log_event(
event="EXECUTION_BLOCKED",
message="Execution blocked by market gate",
meta={
"reason": reason,
"eligible_since": last_run,
"checked_at": now.isoformat(),
},
)
debug_event("MARKET_GATE", "market closed", {"reason": reason})
if emit_event_cb:
emit_event_cb(
event="EXECUTION_BLOCKED",
message="Execution blocked by market gate",
meta={
"reason": reason,
"eligible_since": last_run,
"checked_at": now.isoformat(),
},
)
else:
log_event(
event="DEBUG_BEFORE_TRY_EXECUTE",
message="About to call try_execute_sip",
meta={
"last_run": last_run,
"frequency": frequency_label,
"allowed": allowed,
"reason": reason,
"sip_amount": sip_amount,
"broker": type(broker).__name__,
"now": now.isoformat(),
},
)
if emit_event_cb:
emit_event_cb(
event="DEBUG_BEFORE_TRY_EXECUTE",
message="About to call try_execute_sip",
meta={
"last_run": last_run,
"frequency": frequency_label,
"allowed": allowed,
"reason": reason,
"sip_amount": sip_amount,
"broker": type(broker).__name__,
"now": now.isoformat(),
},
)
debug_event(
"TRY_EXECUTE_START",
"calling try_execute_sip",
{"sip_interval_sec": delta.total_seconds(), "sip_amount": sip_amount},
)
state, executed = try_execute_sip(
now=now,
market_open=True,
sip_interval=delta.total_seconds(),
sip_amount=sip_amount,
sp_price=nifty_price,
gd_price=gold_price,
eq_w=eq_w,
gd_w=gd_w,
broker=broker,
mode=mode,
)
log_event(
event="DEBUG_AFTER_TRY_EXECUTE",
message="Returned from try_execute_sip",
meta={
"executed": executed,
"state_last_run": state.get("last_run"),
"state_last_sip_ts": state.get("last_sip_ts"),
},
)
if emit_event_cb:
emit_event_cb(
event="DEBUG_AFTER_TRY_EXECUTE",
message="Returned from try_execute_sip",
meta={
"executed": executed,
"state_last_run": state.get("last_run"),
"state_last_sip_ts": state.get("last_sip_ts"),
},
)
debug_event(
"TRY_EXECUTE_DONE",
"try_execute_sip finished",
{"executed": executed, "last_run": state.get("last_run")},
)
if executed:
log_event("SIP_TRIGGERED", {
"date": now.date().isoformat(),
"allocation": weights,
"cash_used": sip_amount
})
debug_event("SIP_TRIGGERED", "sip executed", {"cash_used": sip_amount})
portfolio_value = (
state["nifty_units"] * nifty_price
+ state["gold_units"] * gold_price
)
log_event("PORTFOLIO_UPDATED", {
"nifty_units": state["nifty_units"],
"gold_units": state["gold_units"],
"portfolio_value": portfolio_value
})
print("SIP executed at", now)
if should_log_mtm(None, now):
logical_time = normalize_logical_time(now)
with db_transaction() as cur:
log_mtm(
nifty_units=state["nifty_units"],
gold_units=state["gold_units"],
nifty_price=nifty_price,
gold_price=gold_price,
total_invested=state["total_invested"],
cur=cur,
logical_time=logical_time,
)
broker.update_equity(
{NIFTY: nifty_price, GOLD: gold_price},
now,
cur=cur,
logical_time=logical_time,
)
sleep_with_heartbeat(30, stop_event, scope_user, scope_run)
except Exception as e:
_set_state(scope_user, scope_run, state="ERROR", last_heartbeat_ts=datetime.utcnow().isoformat() + "Z")
_update_engine_status(scope_user, scope_run, "ERROR")
log_event("ENGINE_ERROR", {"error": str(e)})
raise
log_event("ENGINE_STOP")
_set_state(
scope_user,
scope_run,
state="STOPPED",
last_heartbeat_ts=datetime.utcnow().isoformat() + "Z",
)
_update_engine_status(scope_user, scope_run, "STOPPED")
print("Strategy engine stopped")
_clear_runner(scope_user, scope_run)
def start_engine(config):
user_id = config.get("user_id")
run_id = config.get("run_id")
if not user_id:
raise ValueError("user_id is required to start engine")
if not run_id:
raise ValueError("run_id is required to start engine")
with _RUNNERS_LOCK:
key = _state_key(user_id, run_id)
runner = _RUNNERS.get(key)
if runner and runner["thread"].is_alive():
return False
stop_event = threading.Event()
thread = threading.Thread(
target=_engine_loop,
args=(config, stop_event),
daemon=True,
)
_RUNNERS[key] = {"thread": thread, "stop_event": stop_event}
thread.start()
return True
def stop_engine(user_id: str, run_id: str | None = None, timeout: float | None = 10.0):
runners = []
with _RUNNERS_LOCK:
if run_id:
key = _state_key(user_id, run_id)
runner = _RUNNERS.get(key)
if runner:
runners.append((key, runner))
else:
for key, runner in list(_RUNNERS.items()):
if key[0] == user_id:
runners.append((key, runner))
for _key, runner in runners:
runner["stop_event"].set()
stopped_all = True
for key, runner in runners:
thread = runner["thread"]
if timeout is not None:
thread.join(timeout=timeout)
stopped = not thread.is_alive()
if stopped:
_clear_runner(key[0], key[1])
else:
stopped_all = False
return stopped_all

View File

@ -0,0 +1,303 @@
# engine/state.py
from datetime import datetime, timezone
from indian_paper_trading_strategy.engine.db import db_connection, insert_engine_event, run_with_retry, get_context
DEFAULT_STATE = {
"initial_cash": 0.0,
"cash": 0.0,
"total_invested": 0.0,
"nifty_units": 0.0,
"gold_units": 0.0,
"last_sip_ts": None,
"last_run": None,
"sip_frequency": None,
}
DEFAULT_PAPER_STATE = {
**DEFAULT_STATE,
"initial_cash": 1_000_000.0,
"cash": 1_000_000.0,
"sip_frequency": {"value": 30, "unit": "days"},
}
def _state_key(mode: str | None):
key = (mode or "LIVE").strip().upper()
return "PAPER" if key == "PAPER" else "LIVE"
def _default_state(mode: str | None):
if _state_key(mode) == "PAPER":
return DEFAULT_PAPER_STATE.copy()
return DEFAULT_STATE.copy()
def _local_tz():
return datetime.now().astimezone().tzinfo
def _format_local_ts(value: datetime | None):
if value is None:
return None
return value.astimezone(_local_tz()).replace(tzinfo=None).isoformat()
def _parse_ts(value):
if value is None:
return None
if isinstance(value, datetime):
if value.tzinfo is None:
return value.replace(tzinfo=_local_tz())
return value
if isinstance(value, str):
text = value.strip()
if not text:
return None
try:
parsed = datetime.fromisoformat(text.replace("Z", "+00:00"))
except ValueError:
return None
if parsed.tzinfo is None:
parsed = parsed.replace(tzinfo=_local_tz())
return parsed
return None
def _resolve_scope(user_id: str | None, run_id: str | None):
return get_context(user_id, run_id)
def load_state(
mode: str | None = "LIVE",
*,
cur=None,
for_update: bool = False,
user_id: str | None = None,
run_id: str | None = None,
):
scope_user, scope_run = _resolve_scope(user_id, run_id)
key = _state_key(mode)
if key == "PAPER":
if cur is None:
with db_connection() as conn:
with conn.cursor() as cur:
return load_state(
mode=mode,
cur=cur,
for_update=for_update,
user_id=scope_user,
run_id=scope_run,
)
lock_clause = " FOR UPDATE" if for_update else ""
cur.execute(
f"""
SELECT initial_cash, cash, total_invested, nifty_units, gold_units,
last_sip_ts, last_run, sip_frequency_value, sip_frequency_unit
FROM engine_state_paper
WHERE user_id = %s AND run_id = %s{lock_clause}
LIMIT 1
""",
(scope_user, scope_run),
)
row = cur.fetchone()
if not row:
return _default_state(mode)
merged = _default_state(mode)
merged.update(
{
"initial_cash": float(row[0]) if row[0] is not None else merged["initial_cash"],
"cash": float(row[1]) if row[1] is not None else merged["cash"],
"total_invested": float(row[2]) if row[2] is not None else merged["total_invested"],
"nifty_units": float(row[3]) if row[3] is not None else merged["nifty_units"],
"gold_units": float(row[4]) if row[4] is not None else merged["gold_units"],
"last_sip_ts": _format_local_ts(row[5]),
"last_run": _format_local_ts(row[6]),
}
)
if row[7] is not None or row[8] is not None:
merged["sip_frequency"] = {"value": row[7], "unit": row[8]}
return merged
if cur is None:
with db_connection() as conn:
with conn.cursor() as cur:
return load_state(
mode=mode,
cur=cur,
for_update=for_update,
user_id=scope_user,
run_id=scope_run,
)
lock_clause = " FOR UPDATE" if for_update else ""
cur.execute(
f"""
SELECT total_invested, nifty_units, gold_units, last_sip_ts, last_run
FROM engine_state
WHERE user_id = %s AND run_id = %s{lock_clause}
LIMIT 1
""",
(scope_user, scope_run),
)
row = cur.fetchone()
if not row:
return _default_state(mode)
merged = _default_state(mode)
merged.update(
{
"total_invested": float(row[0]) if row[0] is not None else merged["total_invested"],
"nifty_units": float(row[1]) if row[1] is not None else merged["nifty_units"],
"gold_units": float(row[2]) if row[2] is not None else merged["gold_units"],
"last_sip_ts": _format_local_ts(row[3]),
"last_run": _format_local_ts(row[4]),
}
)
return merged
def init_paper_state(
initial_cash: float,
sip_frequency: dict | None = None,
*,
cur=None,
user_id: str | None = None,
run_id: str | None = None,
):
state = DEFAULT_PAPER_STATE.copy()
state.update(
{
"initial_cash": float(initial_cash),
"cash": float(initial_cash),
"total_invested": 0.0,
"nifty_units": 0.0,
"gold_units": 0.0,
"last_sip_ts": None,
"last_run": None,
"sip_frequency": sip_frequency or state.get("sip_frequency"),
}
)
save_state(state, mode="PAPER", cur=cur, emit_event=True, user_id=user_id, run_id=run_id)
return state
def save_state(
state,
mode: str | None = "LIVE",
*,
cur=None,
emit_event: bool = False,
event_meta: dict | None = None,
user_id: str | None = None,
run_id: str | None = None,
):
scope_user, scope_run = _resolve_scope(user_id, run_id)
key = _state_key(mode)
last_sip_ts = _parse_ts(state.get("last_sip_ts"))
last_run = _parse_ts(state.get("last_run"))
if key == "PAPER":
sip_frequency = state.get("sip_frequency")
sip_value = None
sip_unit = None
if isinstance(sip_frequency, dict):
sip_value = sip_frequency.get("value")
sip_unit = sip_frequency.get("unit")
def _save(cur):
cur.execute(
"""
INSERT INTO engine_state_paper (
user_id, run_id, initial_cash, cash, total_invested, nifty_units, gold_units,
last_sip_ts, last_run, sip_frequency_value, sip_frequency_unit
)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (user_id, run_id) DO UPDATE
SET initial_cash = EXCLUDED.initial_cash,
cash = EXCLUDED.cash,
total_invested = EXCLUDED.total_invested,
nifty_units = EXCLUDED.nifty_units,
gold_units = EXCLUDED.gold_units,
last_sip_ts = EXCLUDED.last_sip_ts,
last_run = EXCLUDED.last_run,
sip_frequency_value = EXCLUDED.sip_frequency_value,
sip_frequency_unit = EXCLUDED.sip_frequency_unit
""",
(
scope_user,
scope_run,
float(state.get("initial_cash", 0.0)),
float(state.get("cash", 0.0)),
float(state.get("total_invested", 0.0)),
float(state.get("nifty_units", 0.0)),
float(state.get("gold_units", 0.0)),
last_sip_ts,
last_run,
sip_value,
sip_unit,
),
)
if emit_event:
insert_engine_event(
cur,
"STATE_UPDATED",
data={
"mode": "PAPER",
"cash": state.get("cash"),
"total_invested": state.get("total_invested"),
"nifty_units": state.get("nifty_units"),
"gold_units": state.get("gold_units"),
"last_sip_ts": state.get("last_sip_ts"),
"last_run": state.get("last_run"),
},
meta=event_meta,
ts=datetime.utcnow().replace(tzinfo=timezone.utc),
)
if cur is not None:
_save(cur)
return
def _op(cur, _conn):
_save(cur)
return run_with_retry(_op)
def _save(cur):
cur.execute(
"""
INSERT INTO engine_state (
user_id, run_id, total_invested, nifty_units, gold_units, last_sip_ts, last_run
)
VALUES (%s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (user_id, run_id) DO UPDATE
SET total_invested = EXCLUDED.total_invested,
nifty_units = EXCLUDED.nifty_units,
gold_units = EXCLUDED.gold_units,
last_sip_ts = EXCLUDED.last_sip_ts,
last_run = EXCLUDED.last_run
""",
(
scope_user,
scope_run,
float(state.get("total_invested", 0.0)),
float(state.get("nifty_units", 0.0)),
float(state.get("gold_units", 0.0)),
last_sip_ts,
last_run,
),
)
if emit_event:
insert_engine_event(
cur,
"STATE_UPDATED",
data={
"mode": "LIVE",
"total_invested": state.get("total_invested"),
"nifty_units": state.get("nifty_units"),
"gold_units": state.get("gold_units"),
"last_sip_ts": state.get("last_sip_ts"),
"last_run": state.get("last_run"),
},
meta=event_meta,
ts=datetime.utcnow().replace(tzinfo=timezone.utc),
)
if cur is not None:
_save(cur)
return
def _op(cur, _conn):
_save(cur)
return run_with_retry(_op)

View File

@ -0,0 +1,12 @@
# engine/strategy.py
import numpy as np
def allocation(sp_price, gd_price, sp_sma, gd_sma,
base=0.6, tilt_mult=1.5,
max_tilt=0.25, min_eq=0.2, max_eq=0.9):
rd = (sp_price / sp_sma) - (gd_price / gd_sma)
tilt = np.clip(-rd * tilt_mult, -max_tilt, max_tilt)
eq_w = np.clip(base * (1 + tilt), min_eq, max_eq)
return eq_w, 1 - eq_w

View File

@ -0,0 +1,41 @@
from datetime import datetime, timedelta
def frequency_to_timedelta(freq: dict) -> timedelta:
value = int(freq.get("value", 0))
unit = freq.get("unit")
if value <= 0:
raise ValueError("Frequency value must be > 0")
if unit == "minutes":
return timedelta(minutes=value)
if unit == "days":
return timedelta(days=value)
raise ValueError(f"Unsupported frequency unit: {unit}")
def normalize_logical_time(ts: datetime) -> datetime:
return ts.replace(microsecond=0)
def compute_logical_time(
now: datetime,
last_run: str | None,
interval_seconds: float | None,
) -> datetime:
base = now
if last_run and interval_seconds:
try:
parsed = datetime.fromisoformat(last_run.replace("Z", "+00:00"))
except ValueError:
parsed = None
if parsed is not None:
if now.tzinfo and parsed.tzinfo is None:
parsed = parsed.replace(tzinfo=now.tzinfo)
elif now.tzinfo is None and parsed.tzinfo:
parsed = parsed.replace(tzinfo=None)
candidate = parsed + timedelta(seconds=interval_seconds)
if now >= candidate:
base = candidate
return normalize_logical_time(base)