Skip to content

43 problem cannot watch strikes#44

Merged
lwtsn merged 2 commits into
mainfrom
43-problem-cannot-watch-strikes
Apr 8, 2026
Merged

43 problem cannot watch strikes#44
lwtsn merged 2 commits into
mainfrom
43-problem-cannot-watch-strikes

Conversation

@lwtsn

@lwtsn lwtsn commented Apr 8, 2026

Copy link
Copy Markdown
Contributor

Resolves: #43

lwtsn added 2 commits April 8, 2026 18:38
…ions

Introduce slow/fast watch tiers for options market observation:

Slow watch: batch ingestor polls full expiration chain via REST every
~30s. After each poll it calls watchlist.SetStrikes so the realtime
ingestor always has fresh contract lists without making HTTP calls
inside WebSocket subscription methods.

Fast watch: realtime ingestor opens a WebSocket order book subscription
per specific contract (WatchInstrument / UnwatchInstrument). Used for
pre-trade liquidity assessment and active position monitoring.

Key fixes:
- floatToString: replace string(rune(f)) with strconv.FormatFloat to
  prevent colliding store keys for fractional strikes
- SubscribeExpirationUpdates no longer makes HTTP calls; contracts are
  resolved entirely from the watchlist
- Stop() cancels context first, waits for goroutines (sync.WaitGroup),
  then runs connector cleanup to prevent concurrent subscribe/unsubscribe
- Pending expirations: if strikes are absent at Start() the expiration
  is retried when WatchEventStrikesUpdated fires
- WebSocketConnector interface extended with SubscribeOrderBook /
  UnsubscribeOrderBook; mock regenerated
- Exported WatchEvent* constants replace bare string literals throughout
Copilot AI review requested due to automatic review settings April 8, 2026 12:14
@lwtsn lwtsn linked an issue Apr 8, 2026 that may be closed by this pull request
@lwtsn
lwtsn merged commit 0f1f934 into main Apr 8, 2026
2 of 5 checks passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses issue #43 (“cannot watch strikes”) by making options realtime subscriptions contract-aware and by introducing a two-tier watch model (slow expiration-wide updates vs fast per-instrument order book watching) backed by watchlist-provided strike discovery.

Changes:

  • Extend the options WebSocket connector interface to subscribe/unsubscribe expiration updates with an explicit contract list, and add per-contract order book subscription methods.
  • Add fast-watch instrument tracking + typed watch events to the options watchlist, and wire the realtime ingestor to react to watchlist events (including retrying expirations once strikes arrive).
  • Fix options store contract key generation by using a correct float-to-string conversion; batch ingestor now populates watchlist strikes.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
pkg/types/connector/options/websocket.go Updates WS connector API for contract-aware expiration subs + per-contract order books.
pkg/markets/options/watchlist.go Adds fast-watch instrument tracking and emits typed watch events.
pkg/markets/options/types/watchlist.go Documents/defines slow vs fast watch tiers and adds event type constants + new watchlist methods.
pkg/markets/options/store/store.go Fixes float strike-to-string conversion used in contract map keys.
pkg/markets/options/ingestor/realtime/ingestor.go Resolves expiration contracts from watchlist strikes, adds pending retry logic, and handles instrument watch events for order books.
pkg/markets/options/ingestor/realtime/ingestor_test.go Updates mocks/calls for new expiration subscription signature; pre-populates strikes for tests.
pkg/markets/options/ingestor/batch/ingestor.go Collects strikes during REST polling and updates watchlist via SetStrikes.
mocks/.../options/WebSocketConnector.go Regenerated mock to match updated connector interface (incl. new methods).
doc.go Adds top-level package documentation for the module.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +177 to +181
switch ev.Type {
case optionsTypes.WatchEventInstrumentWatched:
if ev.Contract == nil {
continue
}

Copilot AI Apr 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

processWatchlistEvents only reacts to InstrumentWatched/Unwatched and StrikesUpdated, but it ignores ExpirationAdded/ExpirationRemoved events emitted by the watchlist. If a strategy/asset loader requires or releases expirations after the realtime ingestor has started, the connector will never subscribe/unsubscribe for those expirations, so updates won’t flow (and pending-strike retries won’t be set up). Add cases for WatchEventExpirationAdded/Removed to call subscribeExpiration and to unsubscribe/clear any pending state when an expiration is removed.

Copilot uses AI. Check for mistakes.
Comment on lines +71 to 75
// Collect all strikes and store market data for each contract
strikes := make([]float64, 0, len(expirationData))
for strike, optionTypes := range expirationData {
strikes = append(strikes, strike)
for optionType, optionData := range optionTypes {

Copilot AI Apr 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

strikes is built by ranging over expirationData (a map), so the slice order is nondeterministic. That means SetStrikes may publish a different ordering on each poll even when the strike set is unchanged, which can cause noisy StrikesUpdated events and unstable downstream behavior. Sort (and optionally dedupe) strikes before calling SetStrikes, and consider skipping SetStrikes when the stored strikes are already equal.

Copilot uses AI. Check for mistakes.
Comment on lines +157 to +163
key := instrumentKey{
Exchange: exchange,
Pair: contract.Pair,
Expiration: contract.Expiration,
Strike: contract.Strike,
OptionType: contract.OptionType,
}

Copilot AI Apr 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instrumentKey uses contract.OptionType as-is. Elsewhere the code normalizes option types to uppercase (e.g., store keys use strings.ToUpper and realtime uses "CALL"/"PUT"), so passing "call" vs "CALL" would create duplicate fast-watch entries and make UnwatchInstrument fail to match. Normalize OptionType (e.g., strings.ToUpper) when building instrumentKey (and ideally when emitting/storing) to make the behavior consistent.

Copilot uses AI. Check for mistakes.
Comment on lines +188 to +194
key := instrumentKey{
Exchange: exchange,
Pair: contract.Pair,
Expiration: contract.Expiration,
Strike: contract.Strike,
OptionType: contract.OptionType,
}

Copilot AI Apr 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same OptionType normalization issue as WatchInstrument: UnwatchInstrument builds instrumentKey from contract.OptionType without normalization, so callers using different casing than what was originally watched won’t be able to remove the instrument. Normalize OptionType when constructing the key so watch/unwatch are consistent.

Copilot uses AI. Check for mistakes.
Comment on lines +96 to 99
// Update the watchlist with the discovered strikes so the realtime
// ingestor can resolve instruments without making REST calls
o.watchlist.SetStrikes(exchangeName, pair, expiration, strikes)
}

Copilot AI Apr 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CollectNow now updates the watchlist via SetStrikes, but the batch ingestor tests only assert store writes. Add assertions that SetStrikes results in watchlist.GetAvailableStrikes returning the discovered strikes (ideally in sorted order) so the realtime ingestor contract-resolution path is exercised.

Copilot uses AI. Check for mistakes.
Comment on lines +162 to +206
func (o *optionsRealtimeIngestor) processWatchlistEvents() {
defer o.wg.Done()

exchangeName := o.connector.GetConnectorInfo().Name
events := o.watchlist.Subscribe(exchangeName)
defer o.watchlist.Unsubscribe(exchangeName)

for {
select {
case <-o.ctx.Done():
return
case ev, ok := <-events:
if !ok {
return
}
switch ev.Type {
case optionsTypes.WatchEventInstrumentWatched:
if ev.Contract == nil {
continue
}
c := toConnectorContract(*ev.Contract)
if err := o.connector.SubscribeOrderBook(&c); err != nil {
o.logger.Errorf("failed to subscribe order book for %v: %v", ev.Contract, err)
}

case optionsTypes.WatchEventInstrumentUnwatched:
if ev.Contract == nil {
continue
}
c := toConnectorContract(*ev.Contract)
if err := o.connector.UnsubscribeOrderBook(&c); err != nil {
o.logger.Warnf("failed to unsubscribe order book for %v: %v", ev.Contract, err)
}

case optionsTypes.WatchEventStrikesUpdated:
// Retry any expirations that were skipped at Start() due to absent strikes.
o.pendingMu.Lock()
ref := expirationRef{pair: ev.Pair, expiration: ev.Expiration}
_, isPending := o.pendingExpirations[ref]
o.pendingMu.Unlock()

if isPending {
o.subscribeExpiration(exchangeName, ev.Pair, ev.Expiration)
}
}

Copilot AI Apr 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New behavior (pending expirations retry on StrikesUpdated + fast-watch order book subscribe/unsubscribe on instrument events) isn’t covered by tests. Add tests that (1) Start() with a watched expiration but no strikes does not call SubscribeExpirationUpdates, then after SetStrikes triggers StrikesUpdated it does; and (2) WatchInstrument/UnwatchInstrument events lead to SubscribeOrderBook/UnsubscribeOrderBook calls.

Copilot uses AI. Check for mistakes.
SubscribeExpirationUpdates(pair portfolio.Pair, expiration time.Time) error
UnsubscribeExpirationUpdates(pair portfolio.Pair, expiration time.Time) error
// Slow watch — subscribe to ticker updates for all contracts in an expiration.
// contracts is resolved by the realtime ingestor from the watchlist (no REST calls).

Copilot AI Apr 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Grammar: “contracts is resolved” → “contracts are resolved”.

Suggested change
// contracts is resolved by the realtime ingestor from the watchlist (no REST calls).
// contracts are resolved by the realtime ingestor from the watchlist (no REST calls).

Copilot uses AI. Check for mistakes.
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.

Problem: cannot watch strikes

2 participants