Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 27 additions & 11 deletions src/stiq/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ def filter(self, record: logging.LogRecord) -> bool:
app.state.cache = cache_manager
app.state.builder = builder
app.state.provider = provider
app.state.shutdown_timer = None

# Startup — launch the Chrome app window after a brief delay
# so Uvicorn can finish the lifespan and start accepting requests
Expand Down Expand Up @@ -366,21 +367,29 @@ async def update_interval(
return {"success": True, "interval": seconds_val}


async def delayed_exit(delay_secs: float):
async def delayed_exit(
delay_secs: float, usage_tracker: TiingoUsageTracker | None = None
):
await asyncio.sleep(delay_secs)
if usage_tracker:
try:
usage_tracker.save()
except Exception as e:
print(f"[stiq] Error saving usage tracker: {e}", file=sys.stderr)
os._exit(0)


@app.post("/api/shutdown")
async def shutdown(usage_tracker: TiingoUsageTracker = Depends(get_usage_tracker_dep)):
print("[stiq] Shutting down...")
usage_tracker.save()
asyncio.create_task(delayed_exit(0.5))
return {"success": True}


@app.get("/api/stream")
async def stream_events(event_bus: EventBus = Depends(get_event_bus_dep)):
async def stream_events(
request: Request, event_bus: EventBus = Depends(get_event_bus_dep)
):
# Cancel any pending shutdown timer — a client just connected
timer = getattr(request.app.state, "shutdown_timer", None)
if timer and not timer.done():
print("[stiq] Client reconnected, cancelling pending shutdown.")
timer.cancel()
request.app.state.shutdown_timer = None

async def event_generator():
queue: asyncio.Queue = asyncio.Queue()
event_bus.subscribe(queue)
Expand All @@ -392,8 +401,15 @@ async def event_generator():
except asyncio.TimeoutError:
yield {"comment": "ping"}
except asyncio.CancelledError:
pass
finally:
event_bus.unsubscribe(queue)
raise
if len(event_bus.subscribers) == 0:
print("[stiq] Last SSE client disconnected. Shutting down in 3s...")
usage_tracker = getattr(request.app.state, "usage_tracker", None)
request.app.state.shutdown_timer = asyncio.create_task(
delayed_exit(3.0, usage_tracker)
)

return EventSourceResponse(event_generator())

Expand Down
5 changes: 3 additions & 2 deletions src/stiq/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,9 @@ def is_market_open(self) -> bool:
return False

# Standard hours: 9:30 AM to 4:00 PM EST
# (Using 9 <= hour < 16 acts as 9:00 AM - 4:00 PM, which is a good baseline)
return 9 <= now_et.hour < 16
start_time = now_et.replace(hour=9, minute=30, second=0, microsecond=0)
end_time = now_et.replace(hour=16, minute=0, second=0, microsecond=0)
return start_time <= now_et < end_time

def build_realtime_quote(
self,
Expand Down
10 changes: 9 additions & 1 deletion tests/test_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,19 @@ def test_is_market_open(mock_dt, builder):
mock_dt.date = MagicMock()
assert builder.is_market_open() is True

# 2. Test normal weekday, closed hours (Tuesday, 8:00 AM)
# 2. Test normal weekday, closed hours (Tuesday, 8:00 AM and 9:15 AM)
dt_closed_early = et.localize(datetime(2026, 3, 3, 8, 0, 0))
mock_dt.now.return_value = dt_closed_early
assert builder.is_market_open() is False

dt_closed_915 = et.localize(datetime(2026, 3, 3, 9, 15, 0))
mock_dt.now.return_value = dt_closed_915
assert builder.is_market_open() is False

dt_open_930 = et.localize(datetime(2026, 3, 3, 9, 30, 0))
mock_dt.now.return_value = dt_open_930
assert builder.is_market_open() is True

# 3. Test normal weekday, closed hours (Tuesday, 5:00 PM)
dt_closed_late = et.localize(datetime(2026, 3, 3, 17, 0, 0))
mock_dt.now.return_value = dt_closed_late
Expand Down
7 changes: 2 additions & 5 deletions web/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,11 +134,8 @@ function stiq() {
// Connect to Server-Sent Events stream
this.setupSSE();

// Notify backend when window is closed
window.addEventListener("beforeunload", () => {
navigator.sendBeacon("/api/shutdown");
});



if (this.quotesProvider === "tiingo") {
setInterval(() => {
this.lastUpdated = new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
Expand Down
Loading