Fix live market time handling in strategy engine

This commit is contained in:
Thigazhezhilan J 2026-04-02 09:59:09 +05:30
parent f5bd36df64
commit 88ea093525
6 changed files with 44 additions and 31 deletions

View File

@ -4,12 +4,13 @@ import sys
import threading import threading
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from pathlib import Path from pathlib import Path
from zoneinfo import ZoneInfo
ENGINE_ROOT = Path(__file__).resolve().parents[3] ENGINE_ROOT = Path(__file__).resolve().parents[3]
if str(ENGINE_ROOT) not in sys.path: if str(ENGINE_ROOT) not in sys.path:
sys.path.append(str(ENGINE_ROOT)) sys.path.append(str(ENGINE_ROOT))
from indian_paper_trading_strategy.engine.market import is_market_open, align_to_market_open from indian_paper_trading_strategy.engine.market import is_market_open, align_to_market_open, market_now
from indian_paper_trading_strategy.engine.runner import start_engine, stop_engine from indian_paper_trading_strategy.engine.runner import start_engine, stop_engine
from indian_paper_trading_strategy.engine.state import init_paper_state, load_state, save_state from indian_paper_trading_strategy.engine.state import init_paper_state, load_state, save_state
from indian_paper_trading_strategy.engine.broker import PaperBroker from indian_paper_trading_strategy.engine.broker import PaperBroker
@ -38,6 +39,7 @@ SEQ_LOCK = threading.Lock()
SEQ = 0 SEQ = 0
LAST_WAIT_LOG_TS = {} LAST_WAIT_LOG_TS = {}
WAIT_LOG_INTERVAL = timedelta(seconds=60) WAIT_LOG_INTERVAL = timedelta(seconds=60)
IST = ZoneInfo("Asia/Kolkata")
def init_log_state(): def init_log_state():
global SEQ global SEQ
@ -154,7 +156,7 @@ def _maybe_parse_json(value):
def _local_tz(): def _local_tz():
return datetime.now().astimezone().tzinfo return IST
def _format_local_ts(value: datetime | None): def _format_local_ts(value: datetime | None):
@ -290,7 +292,7 @@ def reactivate_strategy_config(user_id: str, run_id: str):
return cfg return cfg
def _write_status(user_id: str, run_id: str, status): def _write_status(user_id: str, run_id: str, status):
now_local = datetime.now().astimezone() now_local = market_now()
with db_connection() as conn: with db_connection() as conn:
with conn: with conn:
with conn.cursor() as cur: with conn.cursor() as cur:
@ -755,7 +757,11 @@ def get_strategy_status(user_id: str):
if next_eligible: if next_eligible:
try: try:
parsed_next = datetime.fromisoformat(next_eligible) parsed_next = datetime.fromisoformat(next_eligible)
now_cmp = datetime.now(parsed_next.tzinfo) if parsed_next.tzinfo else datetime.now() now_cmp = (
datetime.now(parsed_next.tzinfo)
if parsed_next.tzinfo
else market_now().replace(tzinfo=None)
)
if parsed_next > now_cmp: if parsed_next > now_cmp:
status["status"] = "WAITING" status["status"] = "WAITING"
except ValueError: except ValueError:
@ -928,7 +934,7 @@ def _issue_is_stale_for_current_state(
return True return True
if event == "EXECUTION_BLOCKED" and reason_key == "market_closed": if event == "EXECUTION_BLOCKED" and reason_key == "market_closed":
return is_market_open(datetime.now()) return is_market_open(market_now())
if mode != "LIVE": if mode != "LIVE":
return False return False
@ -1026,7 +1032,7 @@ def get_strategy_summary(user_id: str):
return summary return summary
def get_market_status(): def get_market_status():
now = datetime.now() now = market_now()
return { return {
"status": "OPEN" if is_market_open(now) else "CLOSED", "status": "OPEN" if is_market_open(now) else "CLOSED",
"checked_at": now.isoformat(), "checked_at": now.isoformat(),

View File

@ -12,6 +12,7 @@ from psycopg2.extras import execute_values
from indian_paper_trading_strategy.engine.data import fetch_live_price 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 from indian_paper_trading_strategy.engine.db import db_connection, insert_engine_event, run_with_retry, get_context
from indian_paper_trading_strategy.engine.market import market_now
class Broker(ABC): class Broker(ABC):
@ -50,7 +51,7 @@ class BrokerAuthExpired(BrokerError):
def _local_tz(): def _local_tz():
return datetime.now().astimezone().tzinfo return market_now().tzinfo
def _format_utc_ts(value: datetime | None): def _format_utc_ts(value: datetime | None):

View File

@ -5,6 +5,7 @@ from indian_paper_trading_strategy.engine.state import load_state, save_state
from indian_paper_trading_strategy.engine.broker import Broker, BrokerAuthExpired from indian_paper_trading_strategy.engine.broker import Broker, BrokerAuthExpired
from indian_paper_trading_strategy.engine.ledger import log_event, event_exists from indian_paper_trading_strategy.engine.ledger import log_event, event_exists
from indian_paper_trading_strategy.engine.db import insert_engine_event, run_with_retry from indian_paper_trading_strategy.engine.db import insert_engine_event, run_with_retry
from indian_paper_trading_strategy.engine.market import market_now
from indian_paper_trading_strategy.engine.time_utils import compute_logical_time from indian_paper_trading_strategy.engine.time_utils import compute_logical_time
def _as_float(value): def _as_float(value):
@ -21,7 +22,7 @@ def _as_float(value):
return float(value) return float(value)
def _local_tz(): def _local_tz():
return datetime.now().astimezone().tzinfo return market_now().tzinfo
def _normalize_now(now): def _normalize_now(now):

View File

@ -6,6 +6,10 @@ _MARKET_TZ = pytz.timezone("Asia/Kolkata")
_OPEN_T = dtime(9, 15) _OPEN_T = dtime(9, 15)
_CLOSE_T = dtime(15, 30) _CLOSE_T = dtime(15, 30)
def market_now() -> datetime:
return datetime.now(_MARKET_TZ)
def _as_market_tz(value: datetime) -> datetime: def _as_market_tz(value: datetime) -> datetime:
if value.tzinfo is None: if value.tzinfo is None:
return _MARKET_TZ.localize(value) return _MARKET_TZ.localize(value)
@ -16,7 +20,7 @@ def is_market_open(now: datetime) -> bool:
return now.weekday() < 5 and _OPEN_T <= now.time() <= _CLOSE_T return now.weekday() < 5 and _OPEN_T <= now.time() <= _CLOSE_T
def india_market_status(): def india_market_status():
now = datetime.now(_MARKET_TZ) now = market_now()
return is_market_open(now), now return is_market_open(now), now

View File

@ -5,7 +5,7 @@ from datetime import datetime, timedelta, timezone
from psycopg2.extras import Json from psycopg2.extras import Json
from indian_paper_trading_strategy.engine.market import is_market_open, align_to_market_open from indian_paper_trading_strategy.engine.market import is_market_open, align_to_market_open, market_now
from indian_paper_trading_strategy.engine.execution import try_execute_sip from indian_paper_trading_strategy.engine.execution import try_execute_sip
from indian_paper_trading_strategy.engine.broker import PaperBroker, LiveZerodhaBroker, BrokerAuthExpired from indian_paper_trading_strategy.engine.broker import PaperBroker, LiveZerodhaBroker, BrokerAuthExpired
from indian_paper_trading_strategy.engine.mtm import log_mtm, should_log_mtm from indian_paper_trading_strategy.engine.mtm import log_mtm, should_log_mtm
@ -315,7 +315,7 @@ def _engine_loop(config, stop_event: threading.Event):
# Gate 2: time to SIP # Gate 2: time to SIP
last_run = _last_execution_anchor(state, mode) last_run = _last_execution_anchor(state, mode)
is_first_run = last_run is None is_first_run = last_run is None
now = datetime.now() now = market_now()
debug_event( debug_event(
"ENGINE_LOOP_TICK", "ENGINE_LOOP_TICK",
"engine loop tick", "engine loop tick",

View File

@ -2,6 +2,7 @@
from datetime import datetime, timezone from datetime import datetime, timezone
from indian_paper_trading_strategy.engine.db import db_connection, insert_engine_event, run_with_retry, get_context from indian_paper_trading_strategy.engine.db import db_connection, insert_engine_event, run_with_retry, get_context
from indian_paper_trading_strategy.engine.market import market_now
DEFAULT_STATE = { DEFAULT_STATE = {
"initial_cash": 0.0, "initial_cash": 0.0,
@ -31,7 +32,7 @@ def _default_state(mode: str | None):
return DEFAULT_STATE.copy() return DEFAULT_STATE.copy()
def _local_tz(): def _local_tz():
return datetime.now().astimezone().tzinfo return market_now().tzinfo
def _format_local_ts(value: datetime | None): def _format_local_ts(value: datetime | None):
if value is None: if value is None: