diff --git a/backend/app/services/strategy_service.py b/backend/app/services/strategy_service.py index c249e37..8b606bb 100644 --- a/backend/app/services/strategy_service.py +++ b/backend/app/services/strategy_service.py @@ -4,12 +4,13 @@ import sys import threading from datetime import datetime, timedelta, timezone from pathlib import Path +from zoneinfo import ZoneInfo ENGINE_ROOT = Path(__file__).resolve().parents[3] if str(ENGINE_ROOT) not in sys.path: 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.state import init_paper_state, load_state, save_state from indian_paper_trading_strategy.engine.broker import PaperBroker @@ -38,6 +39,7 @@ SEQ_LOCK = threading.Lock() SEQ = 0 LAST_WAIT_LOG_TS = {} WAIT_LOG_INTERVAL = timedelta(seconds=60) +IST = ZoneInfo("Asia/Kolkata") def init_log_state(): global SEQ @@ -154,7 +156,7 @@ def _maybe_parse_json(value): def _local_tz(): - return datetime.now().astimezone().tzinfo + return IST def _format_local_ts(value: datetime | None): @@ -290,7 +292,7 @@ def reactivate_strategy_config(user_id: str, run_id: str): return cfg 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 conn: with conn.cursor() as cur: @@ -755,7 +757,11 @@ def get_strategy_status(user_id: str): if next_eligible: try: 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: status["status"] = "WAITING" except ValueError: @@ -928,7 +934,7 @@ def _issue_is_stale_for_current_state( return True if event == "EXECUTION_BLOCKED" and reason_key == "market_closed": - return is_market_open(datetime.now()) + return is_market_open(market_now()) if mode != "LIVE": return False @@ -1026,7 +1032,7 @@ def get_strategy_summary(user_id: str): return summary def get_market_status(): - now = datetime.now() + now = market_now() return { "status": "OPEN" if is_market_open(now) else "CLOSED", "checked_at": now.isoformat(), diff --git a/indian_paper_trading_strategy/engine/broker.py b/indian_paper_trading_strategy/engine/broker.py index 374e924..3241cbf 100644 --- a/indian_paper_trading_strategy/engine/broker.py +++ b/indian_paper_trading_strategy/engine/broker.py @@ -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.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): @@ -49,8 +50,8 @@ class BrokerAuthExpired(BrokerError): pass -def _local_tz(): - return datetime.now().astimezone().tzinfo +def _local_tz(): + return market_now().tzinfo def _format_utc_ts(value: datetime | None): diff --git a/indian_paper_trading_strategy/engine/execution.py b/indian_paper_trading_strategy/engine/execution.py index e2d5e47..f179cd5 100644 --- a/indian_paper_trading_strategy/engine/execution.py +++ b/indian_paper_trading_strategy/engine/execution.py @@ -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.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.market import market_now from indian_paper_trading_strategy.engine.time_utils import compute_logical_time def _as_float(value): @@ -21,7 +22,7 @@ def _as_float(value): return float(value) def _local_tz(): - return datetime.now().astimezone().tzinfo + return market_now().tzinfo def _normalize_now(now): diff --git a/indian_paper_trading_strategy/engine/market.py b/indian_paper_trading_strategy/engine/market.py index 0a0d938..a999420 100644 --- a/indian_paper_trading_strategy/engine/market.py +++ b/indian_paper_trading_strategy/engine/market.py @@ -1,24 +1,28 @@ # 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) +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 market_now() -> datetime: + return datetime.now(_MARKET_TZ) + +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 india_market_status(): + now = market_now() + + return is_market_open(now), now def next_market_open_after(value: datetime) -> datetime: current = _as_market_tz(value) diff --git a/indian_paper_trading_strategy/engine/runner.py b/indian_paper_trading_strategy/engine/runner.py index 6d330b0..40c6784 100644 --- a/indian_paper_trading_strategy/engine/runner.py +++ b/indian_paper_trading_strategy/engine/runner.py @@ -5,7 +5,7 @@ from datetime import datetime, timedelta, timezone 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.broker import PaperBroker, LiveZerodhaBroker, BrokerAuthExpired 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 last_run = _last_execution_anchor(state, mode) is_first_run = last_run is None - now = datetime.now() + now = market_now() debug_event( "ENGINE_LOOP_TICK", "engine loop tick", diff --git a/indian_paper_trading_strategy/engine/state.py b/indian_paper_trading_strategy/engine/state.py index ef15553..d14998c 100644 --- a/indian_paper_trading_strategy/engine/state.py +++ b/indian_paper_trading_strategy/engine/state.py @@ -1,7 +1,8 @@ # 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 +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.market import market_now DEFAULT_STATE = { "initial_cash": 0.0, @@ -30,8 +31,8 @@ def _default_state(mode: str | None): return DEFAULT_PAPER_STATE.copy() return DEFAULT_STATE.copy() -def _local_tz(): - return datetime.now().astimezone().tzinfo +def _local_tz(): + return market_now().tzinfo def _format_local_ts(value: datetime | None): if value is None: