Skip to content

Add price signals and cross-symbol signal support#233

Merged
michaelchu merged 5 commits intomainfrom
claude/price-signals-cross-symbol
Feb 28, 2026
Merged

Add price signals and cross-symbol signal support#233
michaelchu merged 5 commits intomainfrom
claude/price-signals-cross-symbol

Conversation

@michaelchu
Copy link
Copy Markdown
Member

Summary

  • Add 14 new first-class price signals in optopsy/signals/price.py: price level (above/below/cross), gaps, N-period breakouts, daily returns, drawdown/rally from high/low, and consecutive up/down streaks
  • Add cross-symbol signal support via signal_symbol parameter on build_signal and build_custom_signal, enabling use cases like "use VIX data to trigger entries on SPX options"
  • Update agent system prompt, tool schemas, and model descriptions to document all new signals and the cross-symbol workflow

Test plan

  • 28 new tests in tests/test_signals_price.py covering all 14 signals + edge cases
  • Full test suite passes (1862 tests)
  • ruff check and ruff format clean
  • ty type checker passes

🤖 Generated with Claude Code

Add 14 new price-related signals in optopsy/signals/price.py:
- Price level: price_above, price_below, price_cross_above, price_cross_below
- Gaps: gap_up, gap_down
- Breakouts: high_of_n_days, low_of_n_days
- Returns: daily_return_above, daily_return_below
- Drawdown: drawdown_from_high, rally_from_low
- Streaks: consecutive_up, consecutive_down

Add cross-symbol signal support via signal_symbol parameter on
build_signal and build_custom_signal, enabling use cases like
"use VIX data to trigger entries on SPX options."

Update agent system prompt, tool schemas, and signal descriptions
to document all new signals and cross-symbol workflow.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@michaelchu michaelchu requested a review from Copilot February 27, 2026 23:34
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new suite of first-class “price” signals to the optopsy.signals API and extends the UI toolchain to support cross-symbol signal computation (compute signals on one ticker’s OHLCV and apply them to another ticker’s options dataset).

Changes:

  • Introduces 14 new price-based signals (levels, crossovers, gaps, breakouts, returns, drawdown/rally, streaks) and exports them from optopsy.signals.
  • Adds signal_symbol support to build_signal / build_custom_signal, including yfinance fetching for an explicit symbol and remapping signal dates to the options dataset symbols.
  • Updates tool registry/schemas/models and agent prompt docs; adds a new test module covering the new price signals.

Reviewed changes

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

Show a summary per file
File Description
tests/test_signals_price.py Adds signal-level pytest coverage for the new price signal functions.
optopsy/signals/price.py Implements the new price signals (state + event style signals).
optopsy/signals/_helpers.py Adds _get_open() helper used by gap signals.
optopsy/signals/__init__.py Exports new price signals (and _get_open) from the package API.
optopsy/ui/tools/_signals_builder.py Adds signal_symbol support to build_signal / build_custom_signal and remaps cross-symbol signal dates.
optopsy/ui/tools/_helpers.py Adds _fetch_stock_data_for_symbol() to fetch OHLCV for a single ticker for cross-symbol workflows.
optopsy/ui/tools/_schemas.py Registers the new signal names + params and marks relevant ones as OHLC-requiring.
optopsy/ui/tools/_models.py Documents new signals and adds signal_symbol to the tool argument models.
optopsy/ui/agent.py Updates the agent system prompt documentation for the new signals and cross-symbol workflow.

Comment thread optopsy/ui/tools/_signals_builder.py
Comment thread optopsy/ui/tools/_helpers.py Outdated
Comment thread optopsy/ui/tools/_signals_builder.py Outdated
Comment thread optopsy/ui/tools/_signals_builder.py
- Strip whitespace from signal_symbol to prevent cache key issues
- Gate cross-symbol remapping on needs_stock to avoid duplicating
  rows when signal_symbol is set on IV or date-only signals

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Extract _fetch_single_symbol_stock from the duplicated fetch logic
  in _fetch_stock_data_for_signals and _fetch_stock_data_for_symbol
- Extract _remap_cross_symbol_dates from the duplicated remap blocks
  in _handle_build_signal and _handle_build_custom_signal
- Extract _STOCK_COLS constant for the shared column list

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

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

Comment thread optopsy/ui/tools/_schemas.py Outdated
Comment thread optopsy/ui/tools/_helpers.py
Comment thread tests/test_signals_price.py
Comment thread tests/test_signals_price.py
Comment thread optopsy/signals/price.py
- Add _OPEN_SIGNALS set and validate open column for gap_up/gap_down
- Add days >= 1 validation to consecutive_up/consecutive_down
- Make gap and high_of_n_days tests assert exact firing indices

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

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

Comment on lines +54 to +92
_STOCK_COLS = [
"underlying_symbol",
"quote_date",
"open",
"high",
"low",
"close",
"volume",
]


def _fetch_single_symbol_stock(
symbol: str,
padded_start: "date",
date_max: "date",
) -> pd.DataFrame | None:
"""Fetch and slice OHLCV data for one symbol.

Reads from (or populates) the yfinance parquet cache, then slices to
``[padded_start, date_max]``. Returns a DataFrame with ``_STOCK_COLS``
or ``None`` on failure.
"""
try:
cached = _yf_cache.read(_YF_CACHE_CATEGORY, symbol)
cached = _yf_fetch_and_cache(symbol, cached, date_max)

if cached is None or cached.empty:
return None

result = cached[
(pd.to_datetime(cached["date"]).dt.date >= padded_start)
& (pd.to_datetime(cached["date"]).dt.date <= date_max)
].rename(columns={"date": "quote_date"})
if result.empty:
return None
return result[_STOCK_COLS]
except (OSError, ValueError, KeyError, pd.errors.ParserError) as exc:
_log.warning("yfinance fetch failed for %s: %s", symbol, exc)
return None
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

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

_fetch_single_symbol_stock() always returns result[_STOCK_COLS], but _normalise_yf_df() can legitimately omit optional columns (e.g. volume / open / high / low) when yfinance doesn’t provide them. In that case this selection raises KeyError, causing the fetch to fail even for signals that only need close (and preventing the more specific missing-column validations from triggering). Consider returning the intersection of available columns while enforcing a minimal required set (at least underlying_symbol, quote_date, close), and letting callers validate additional columns as needed.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Valid concern. _fetch_single_symbol_stock now selects only available columns ([c for c in _STOCK_COLS if c in result.columns]) instead of hard-coding all 7, and returns None only if close is missing. This lets signals that only need close work even when yfinance omits optional columns, while letting the caller-level validations (_OHLC_SIGNALS, _OPEN_SIGNALS, _VOLUME_SIGNALS) report specific missing-column errors. Fixed in 79c8c24.

Comment on lines 605 to 642
# Signals that require OHLC data (high, low, close) — need stock data fetch.
_OHLC_SIGNALS = frozenset(
{
"stoch_below",
"stoch_above",
"willr_below",
"willr_above",
"cci_below",
"cci_above",
"uo_above",
"uo_below",
"squeeze_on",
"squeeze_off",
"ao_above",
"ao_below",
"kc_above_upper",
"kc_below_lower",
"donchian_above_upper",
"donchian_below_lower",
"natr_above",
"natr_below",
"massi_above",
"massi_below",
"adx_above",
"adx_below",
"aroon_cross_above",
"aroon_cross_below",
"supertrend_buy",
"supertrend_sell",
"psar_buy",
"psar_sell",
"chop_above",
"chop_below",
"gap_up",
"gap_down",
"high_of_n_days",
"low_of_n_days",
}
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

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

_OHLC_SIGNALS is documented/validated as requiring high, low, and close, but it now includes gap_up/gap_down and high_of_n_days/low_of_n_days. The new price signals don’t actually need all of high+low (and gap signals don’t use high/low at all), so the build_signal OHLC validation can reject otherwise-computable signals and produce a misleading error. Consider splitting these into more precise requirement sets (e.g. close-only vs open+close vs full OHLC) and validating only the columns each signal truly needs.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good point. Removed gap_up/gap_down from _OHLC_SIGNALS — they don't need high/low and already have their own _OPEN_SIGNALS validation checking for the open column. high_of_n_days/low_of_n_days remain in _OHLC_SIGNALS since they genuinely use the high/low columns. Fixed in 79c8c24.

Comment thread tests/test_signals_price.py Outdated
Comment on lines +192 to +200
result = high_of_n_days(period=5)(data)
# Bar 7: close=103, prev 5-bar high max = max(103,99,98,100,104)
# Wait — rolling high is on the high column, shifted by 1
# Bar 7 rolling(5).max().shift(1) = max of bars 2-6 highs = max(101,103,99,98,100) = 103
# close=103 >= 103 → True
assert result.iloc[7] == True
# Bar 9: close=105, prev 5-bar high = max of bars 4-8 = max(99,98,100,104,103) = 104
# 105 >= 104 → True
assert result.iloc[9] == True
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

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

test_high_of_n_days_breakout only asserts that a couple of bars are True, so it would still pass if high_of_n_days() incorrectly fires on additional bars (false positives). To make the test more regression-proof, consider asserting the full expected boolean vector or the exact list of firing indices for this fixture.

Copilot generated this review using guidance from repository custom instructions.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Agreed. Updated to assert exact firing indices: assert fire_indices == [1, 3, 7, 9]. Added per-bar computation comments for clarity. Fixed in 79c8c24.

Comment on lines +214 to +218
result = low_of_n_days(period=5)(data)
# Bar 7: close=96, prev 5-bar low = min of bars 2-6 lows = min(99,101,97,96,98) = 96
# 96 <= 96 → True
assert result.iloc[7] == True

Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

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

test_low_of_n_days_breakdown currently asserts only one True bar. This can miss regressions where low_of_n_days() produces extra True bars. Consider asserting the exact firing indices (or full boolean vector) for the entire series so the test fails on false positives as well.

Copilot generated this review using guidance from repository custom instructions.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Agreed. Updated to assert exact firing indices: assert fire_indices == [4, 5, 7, 8, 9]. Added per-bar computation comments. Fixed in 79c8c24.

- _fetch_single_symbol_stock now selects only available columns from
  _STOCK_COLS instead of hard-coding all 7, preventing KeyError when
  yfinance omits optional columns (open/high/low/volume)
- Remove gap_up/gap_down from _OHLC_SIGNALS (they need open+close,
  not high/low); they already have their own _OPEN_SIGNALS validation
- Make high_of_n_days and low_of_n_days tests assert exact fire indices

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.

Comment thread optopsy/ui/tools/_helpers.py
@michaelchu michaelchu merged commit 45e15b6 into main Feb 28, 2026
12 checks passed
@michaelchu michaelchu deleted the claude/price-signals-cross-symbol branch February 28, 2026 00:35
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.

2 participants