fix: stop a dead price feed from stranding expired positions - #176
Merged
Conversation
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.
Code Review SummaryStatus: No Issues Found | Recommendation: Merge Files Reviewed (4 files)
Reviewed by step-3.7-flash · Input: 66.6K · Output: 12K · Cached: 360.2K |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
OPEN7.5 hours later, and the CLIprocess 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: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_pricerequested the wrong data feedmarket_data/client.pybuilt itsStockBarsRequestwithout afeed, so Alpacadefaulted to SIP, which the free subscription rejects. The other two requests in the
same file (
_data_fetch,get_price_at) already passfeed=DataFeed.IEX. This onewas missed.
2. Window expiry was gated behind the live price fetch
monitor/monitor.py:_check_positionfetched the latest price as its firststatement. 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
OPENforever 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)- thecandle 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
mainand pass here.test_get_latest_price_requests_the_iex_feedtest_window_expiry_closes_even_when_latest_price_is_unavailable- the regressiontest for the hang
test_window_expiry_prices_the_exit_at_the_deadline_not_at_discoverytest_expired_window_does_not_fetch_the_latest_price641 passed, 100% coverage,
ruff check alphoryn/ tests/clean. Suite verified underCI's credential-less environment (
GOOGLE_APPLICATION_CREDENTIALS=/nonexistent/adc.json),not just locally.
Not in this PR
OPENin the local memory bank. Per FR-019 thatblocks 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.mdwas updated with the new check order, butspecs/is gitignoredas of chore: untrack local specs and telemetry docs #175, so that edit stays local.
feedback_evaluationsstill has 0 rows. The feedback agent has never run live, becauseno position has ever closed. That becomes testable once this ships.