Fixed Errors
This commit is contained in:
parent
82098ff9e5
commit
db7315911b
@ -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()
|
||||
|
||||
|
||||
@ -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
@ -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,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@ -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}")
|
||||
|
||||
324
indian_paper_trading_strategy/engine/db.py
Normal file
324
indian_paper_trading_strategy/engine/db.py
Normal 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,
|
||||
),
|
||||
)
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
122
indian_paper_trading_strategy/storage/history/GOLDBEES.NS.csv
Normal file
122
indian_paper_trading_strategy/storage/history/GOLDBEES.NS.csv
Normal 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
|
||||
|
122
indian_paper_trading_strategy/storage/history/NIFTYBEES.NS.csv
Normal file
122
indian_paper_trading_strategy/storage/history/NIFTYBEES.NS.csv
Normal 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
|
||||
|
565
indian_paper_trading_strategy/storage/ledger.csv.bak
Normal file
565
indian_paper_trading_strategy/storage/ledger.csv.bak
Normal 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
|
||||
17
indian_paper_trading_strategy/storage/mtm_ledger.csv.bak
Normal file
17
indian_paper_trading_strategy/storage/mtm_ledger.csv.bak
Normal 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
|
||||
88
indian_paper_trading_strategy/storage/paper_broker.json.bak
Normal file
88
indian_paper_trading_strategy/storage/paper_broker.json.bak
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
7
indian_paper_trading_strategy/storage/state.json.bak
Normal file
7
indian_paper_trading_strategy/storage/state.json.bak
Normal 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"
|
||||
}
|
||||
13
indian_paper_trading_strategy/storage/state_paper.json.bak
Normal file
13
indian_paper_trading_strategy/storage/state_paper.json.bak
Normal 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"
|
||||
}
|
||||
}
|
||||
208
indian_paper_trading_strategy_1/app/streamlit_app.py
Normal file
208
indian_paper_trading_strategy_1/app/streamlit_app.py
Normal 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()
|
||||
|
||||
1
indian_paper_trading_strategy_1/engine/__init__.py
Normal file
1
indian_paper_trading_strategy_1/engine/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Engine package for the India paper trading strategy."""
|
||||
697
indian_paper_trading_strategy_1/engine/broker.py
Normal file
697
indian_paper_trading_strategy_1/engine/broker.py
Normal 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)
|
||||
|
||||
150
indian_paper_trading_strategy_1/engine/config.py
Normal file
150
indian_paper_trading_strategy_1/engine/config.py
Normal 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,
|
||||
),
|
||||
)
|
||||
|
||||
81
indian_paper_trading_strategy_1/engine/data.py
Normal file
81
indian_paper_trading_strategy_1/engine/data.py
Normal 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}")
|
||||
198
indian_paper_trading_strategy_1/engine/engine_runner.py
Normal file
198
indian_paper_trading_strategy_1/engine/engine_runner.py
Normal 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()
|
||||
|
||||
157
indian_paper_trading_strategy_1/engine/execution.py
Normal file
157
indian_paper_trading_strategy_1/engine/execution.py
Normal 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)
|
||||
|
||||
34
indian_paper_trading_strategy_1/engine/history.py
Normal file
34
indian_paper_trading_strategy_1/engine/history.py
Normal 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
|
||||
113
indian_paper_trading_strategy_1/engine/ledger.py
Normal file
113
indian_paper_trading_strategy_1/engine/ledger.py
Normal 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)
|
||||
|
||||
42
indian_paper_trading_strategy_1/engine/market.py
Normal file
42
indian_paper_trading_strategy_1/engine/market.py
Normal 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
|
||||
154
indian_paper_trading_strategy_1/engine/mtm.py
Normal file
154
indian_paper_trading_strategy_1/engine/mtm.py
Normal 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
|
||||
|
||||
518
indian_paper_trading_strategy_1/engine/runner.py
Normal file
518
indian_paper_trading_strategy_1/engine/runner.py
Normal 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
|
||||
|
||||
303
indian_paper_trading_strategy_1/engine/state.py
Normal file
303
indian_paper_trading_strategy_1/engine/state.py
Normal 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)
|
||||
|
||||
12
indian_paper_trading_strategy_1/engine/strategy.py
Normal file
12
indian_paper_trading_strategy_1/engine/strategy.py
Normal 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
|
||||
41
indian_paper_trading_strategy_1/engine/time_utils.py
Normal file
41
indian_paper_trading_strategy_1/engine/time_utils.py
Normal 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)
|
||||
Loading…
x
Reference in New Issue
Block a user