Skip to content

fix: stop a dead price feed from stranding expired positions - #176

Merged
mohammadp1001 merged 1 commit into
mainfrom
fix/monitor-feed-and-window-expiry
Aug 11, 2026
Merged

fix: stop a dead price feed from stranding expired positions#176
mohammadp1001 merged 1 commit into
mainfrom
fix/monitor-feed-and-window-expiry

Conversation

@mohammadp1001

Copy link
Copy Markdown
Owner

What happened

The first live end-to-end run (2026-08-10) opened a SPY position at 13:42 UTC with a
20-minute evaluation window. The position was still OPEN 7.5 hours later, and the CLI
process was still running, because the CLI may not exit while a position is open
(contracts/agents.md: "The CLI process must remain alive while positions are open").

Every one of the 300 most recent telemetry events was the same MONITOR_ERROR:

{"message":"subscription does not permit querying recent SIP data"}

First occurrence 13:42:32 - 24 seconds after the position opened. The monitor never
successfully checked a position, not once.

Two defects

1. get_latest_price requested the wrong data feed

market_data/client.py built its StockBarsRequest without a feed, so Alpaca
defaulted to SIP, which the free subscription rejects. The other two requests in the
same file (_data_fetch, get_price_at) already pass feed=DataFeed.IEX. This one
was missed.

2. Window expiry was gated behind the live price fetch

monitor/monitor.py:_check_position fetched the latest price as its first
statement. When that raised, the window-expiry check at the bottom of the function
never ran. An expired position could not time out while the feed was down, so it
stayed OPEN forever and took the process with it.

Fixing only defect 1 would have left any network blip able to reproduce the hang.

The fix

Window expiry now runs first, and is priced with get_price_at(ticker, deadline) - the
candle covering the deadline, not the price at the moment the monitor noticed. It is the
one exit that must always be able to fire, so it no longer depends on a live quote.
Stop-loss and profit-target checks still use get_latest_price, below it.

Deliberate behaviour change

An expired window now wins over a stop-loss that the same poll would also have
triggered. Past the deadline the position should already have been closed, so
attributing the exit to later price movement would misreport it. No existing test
covered that overlap.

Tests

Written test-first; all four fail on main and pass here.

  • test_get_latest_price_requests_the_iex_feed
  • test_window_expiry_closes_even_when_latest_price_is_unavailable - the regression
    test for the hang
  • test_window_expiry_prices_the_exit_at_the_deadline_not_at_discovery
  • test_expired_window_does_not_fetch_the_latest_price

641 passed, 100% coverage, ruff check alphoryn/ tests/ clean. Suite verified under
CI's credential-less environment (GOOGLE_APPLICATION_CREDENTIALS=/nonexistent/adc.json),
not just locally.

Not in this PR

  • The stranded SPY position is still OPEN in the local memory bank. Per FR-019 that
    blocks SPY from trading until it closes and its feedback evaluation completes, so it
    needs clearing before the next run. Local data, not code.
  • contracts/agents.md was updated with the new check order, but specs/ is gitignored
    as of chore: untrack local specs and telemetry docs #175, so that edit stays local.
  • feedback_evaluations still has 0 rows. The feedback agent has never run live, because
    no position has ever closed. That becomes testable once this ships.

Two defects found by the first live run (2026-08-10). The monitor failed
every poll for 7.5 hours and never closed anything, so the CLI - which may
not exit while a position is open - hung until killed.

1. get_latest_price built its StockBarsRequest without a feed, so Alpaca
   defaulted to SIP and the free plan rejected it with "subscription does
   not permit querying recent SIP data". The other two requests in that
   file already pass feed=DataFeed.IEX; this one was missed.

2. _check_position fetched the latest price as its first statement, so
   that failure raised before the window-expiry check below it could run.
   An expired position could never time out while the feed was down.

Window expiry now runs first and is priced with get_price_at(deadline)
rather than the price at the moment of discovery, so the one exit that
must always be able to fire no longer depends on a live quote. Fixing
only the feed would have left a network blip able to reproduce the hang.

An expired window now wins over a stop-loss the same poll would also have
triggered: past the deadline the position should already have been closed,
so pricing the exit off later movement would misreport it.
@kilo-code-bot

kilo-code-bot Bot commented Aug 11, 2026

Copy link
Copy Markdown

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (4 files)
  • alphoryn/market_data/client.py - Added feed=DataFeed.IEX to get_latest_price request
  • alphoryn/monitor/monitor.py - Window expiry moved before live price fetch; priced at deadline via get_price_at
  • tests/unit/test_market_data.py - New test verifying IEX feed is requested
  • tests/unit/test_monitor.py - New regression tests for window expiry independence

Reviewed by step-3.7-flash · Input: 66.6K · Output: 12K · Cached: 360.2K

@mohammadp1001
mohammadp1001 merged commit 06f26a1 into main Aug 11, 2026
3 checks passed
@mohammadp1001
mohammadp1001 deleted the fix/monitor-feed-and-window-expiry branch August 11, 2026 09:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant