API Reference
All endpoints are under https://api.tradewave.ai/v1. Every request requires Authorization: Bearer <key>.
Seasonal patterns only. All returns are expressed as percentages. Raw OHLCV data and price levels are never returned. ML is available on every tier, metered per day (free 5/day, unlimited on Pro/Business).
This page is generated from /api/openapi.yaml. Re-run generate_api_docs.py after any contract change to keep it in sync.
Base URL
https://api.tradewave.ai/v1
Authentication
Authorization: Bearer <your-api-key>
Endpoints
The flagship scanner (find_best_opportunities). Fans out over the caller's in-scope markets, scores each candidate by edge_score, and returns ranked PatternCards. ML fields populate inline on ML-eligible markets (ids 0-4, 11) up to the caller's daily ML allowance (free 5/day, unlimited on Pro/Business); ml_remaining_today reflects how many ML calls remain today (null = unlimited). The reusable price-safe scan core is shared briefly across callers, but auth, tier limits, rate limits, ML quota, and ML scoring run for every request. Default Sharpe scans enrich only the requested depth; receipt-dependent filters and alternate rankings use a bounded 50-row head. evaluated_count + enrichment_capped are surfaced, and any partial market failure is explicit in market_failures.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| markets | query | string | csv of ids, list_markets names, or common aliases ('sp500', 'crypto', 'europe'), e.g. '2,11' or 'S&P 500 STOCKS,ETFs'. Unknown tokens are reported in markets_note; out-of-scope markets are skipped with an upgrade note. Default (omitted) = a liquid equities core (DOW 30, NASDAQ 100, S&P 500, ETFs) intersected with your scope, NOT every in-scope market - pass markets explicitly to scan others. |
| window | query | string, default now | 'now' | 'next_2_weeks' | 'next_month' | a 'from..to' range. The underlying primitive (OppList4) is keyed to a SINGLE entry date, so the scan evaluates opportunities AS OF the window's start date and then keeps only setups whose entry_date falls inside the window - it does not re-scan every date in the range. 'now' starts today (~10 trading days wide). Default 'now'. |
| direction | query | string ("long" | "short") | |
| min_win_rate | query | number | filter on historical_win_rate (share of profitable years), NOT ml_win_prob. Rows past the enrichment cap are excluded when set. |
| min_years | query | integer | trust filter - require years_tested >= N |
| min_days | query | integer | minimum pattern length (holding period) in calendar days, e.g. 10 |
| max_days | query | integer | maximum pattern length (holding period) in calendar days, e.g. 90 |
| min_avg_return | query | number | minimum average seasonal profit in PERCENT (e.g. 5 means >= 5%); matches stats.avg_return_pct |
| min_median_return | query | number | minimum median seasonal profit in PERCENT; matches stats.median_return_pct |
| min_sharpe | query | number | minimum Sharpe ratio, e.g. 1.5; matches stats.sharpe_ratio |
| pe_cycle | query | string ("consecutive" | "pe"), default consecutive | presidential election cycle mode: 'consecutive' (default, consecutive years) or 'pe' (the current presidential-cycle position only) |
| years | query | integer, default 10 | pattern LOOKBACK: how many years (5-98, data-dependent) to scan for patterns. In pe mode this is the number of PE-position occurrences. |
| min_winning_years | query | integer | of those `years`, the minimum WINNING years for a pattern to be listed (the win-rate floor; year2). DEFAULTS to ~90% of `years` (so years=20 -> a valid 20-18). Must lie in the market's DETECTION BAND, which is market-specific (~75-90%+ winners; e.g. S&P 500 ~85%, Wilshire ~90%, FOREX Liquid ~70% at a 20-year lookback). Multi-market scan: out-of-band markets are reported in lookback_note (not errored). |
| rank_by | query | string ("edge" | "win_rate" | "sharpe" | "ml" | "avg_return"), default sharpe | ranking key; default sharpe (Sharpe ratio) |
| limit | query | integer, default 25 | tier-capped (free 3, dev 100, pro 1000, business 5000) |
| view | query | string ("full" | "evidence" | "decision" | "table"), default full | response verbosity. 'full' (API default) = complete cards; 'evidence' = full rank-1 winner + lean runners; 'decision' = lean cards; 'table' = compact rows. MCP discovery defaults to evidence. |
| include | query | string ("chart") | include=chart attaches the two-chart evidence data/specifications to the rank-1 winner only. |
Response fields (ScanResponse)
| Field | Type | Description |
|---|---|---|
| generated_at | string | |
| summary | string | the honest one-line lead: how many candidates were evaluated, how many shown, how many carry a high-conviction seasonal pattern - plus any degradation (upstream win-rate outage), enrichment cap, normalization note, or plan cap |
| window | string | the resolved window (e.g. 'now') |
| markets_scanned | array | the requested in-scope market ids; check market_failures before treating the scan as complete |
| market_failures | array | market ids whose upstream request failed and whose results were omitted; normally empty |
| rank_by | string ("edge" | "win_rate" | "sharpe" | "ml" | "avg_return") | |
| view | string ("full" | "evidence" | "decision" | "table") | the verbosity applied (echoes the request) |
| lookback | object | the resolved detection knobs (year1, year2) |
| count | integer | number of cards returned |
| evaluated_count | integer | candidates considered before slicing/enrichment |
| enrichment_capped | boolean | true when only the leading requested/bounded candidate depth was fully receipt-enriched |
| capped_by_plan | boolean | true when the PLAN's opp_limit (not the caller's own limit) was the binding constraint on how many ranked cards came back - the free-tier wall, made visible |
| shown_of_evaluated | string | 'shown of evaluated' as a display string |
| upgrade_url | string | present only when capped_by_plan is true |
| ml_remaining_today | integer | ML calls remaining today for this key (null = unlimited; 0 = daily limit reached). Free 5/day, Dev 100/day, Pro/Business unlimited. |
| opportunities | array | ranked PatternCards. Under view='evidence', rank 1 is full and runners are lean; under decision every card is lean; under table each item is a compact row. |
| lookback_note | string | present only when the (year1, year2) combo is out of band for some scanned markets - it names them (they contributed nothing). Raise min_winning_years to include them. |
| disclaimer | string | exact regulatory disclaimer (envelope level; guarantees it survives a 'table' projection) |
Example response
{
"generated_at": "2026-05-31T18:30:13",
"window": "now",
"rank_by": "sharpe",
"count": 3,
"evaluated_count": 334,
"enrichment_capped": true,
"ml_remaining_today": null,
"opportunities": [
{
"rank": 1,
"symbol": "DOV",
"market": {"id": "2", "name": "S&P 500 STOCKS"},
"direction": "long",
"bias": "bullish",
"setup": {"entry_date": "2026-06-01",
"entry_window": "2026-05-29 to 2026-06-04",
"hold_days": 246, "exit_date": "2027-02-02"},
"edge_score": 97,
"edge_basis": "win_rate 1.00 x sharpe 3.3 x 10y history",
"stats": {"historical_win_rate": 1.0, "sharpe_ratio": 3.31,
"avg_return_pct": 18.49, "median_return_pct": 18.4, "years": "10"},
"ml": null,
"receipts": {
"years_tested": 10, "wins": 10, "losses": 0,
"historical_win_rate": 1.0, "avg_return_pct": 18.49, "median_return_pct": 18.4,
"best_year": {"year": "2019", "return_pct": 25.8},
"worst_year": {"year": "2024", "return_pct": 12.17},
"per_year": [{"year": "2025", "return_pct": 18.41, "result": "win"}],
"curve_summary": {"shape": "strengthens through the hold; seasonal index rising",
"trend": "rising", "change_pts": 8.4,
"peak_day": 240, "trough_day": 0},
"source": "TradeWave seasonal model, 10y lookback", "as_of": "2026-05-31"
},
"next_step": {
"headline": "To act on this, here is a ready-to-place ticket:",
"order_ticket": {"side": "BUY", "symbol": "DOV", "type": "MARKET",
"time_in_force": "DAY", "suggested_entry_date": "2026-06-01",
"suggested_exit_date": "2027-02-02",
"note": "Seasonal hold ~246 calendar days. Size to your own risk."},
"copy_text": "BUY DOV market, day order; plan exit ~2027-02-02 (246d seasonal hold).",
"set_reminder": {"type": "seasonal_entry", "fire_on": "2026-05-31",
"message": "DOV seasonal window opens tomorrow."},
"framing": "TradeWave gives the edge and the timing; you place the trade at your own broker."
},
"headline": "DOV long - enter ~Jun 1, hold 246d. Won 10/10 years, avg +18.5%, Sharpe 3.3.",
"verdict": "Strong, consistent seasonal long.",
"disclaimer": "Educational seasonal pattern + ML research, not personalized investment advice and not a recommendation to buy or sell. Past performance is not indicative of future results.",
"tier_notes": "ML score shown."
}
]
}
analyze_symbol - one symbol fused server-side into one best-setup PatternCard plus the other candidate setups (compact) and receipts, from OppBySymbol + ChartData4 stats + the consolidated seasonal curve (+ MLScore, metered per day on every tier). Replaces the multi-call stitch and guarantees win_rate consistency across the response. Pin a SPECIFIC setup with entry_date (+days_out) or a period/reverse preset - the "click this exact opportunity / change the date range" flow - to deep-dive THAT window instead of the auto-picked best; pe_cycle + years are the same wave-viewer knobs as on /seasonal-chart.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| symbolrequired | path | string | |
| market | query | string | permanent resource key '0'..'16'. Optional - resolved if the symbol is unique. |
| direction | query | string ("long" | "short") | |
| days_out | query | integer | holding period in days (internal: daysOut). With entry_date, pins the exact window. |
| entry_date | query | string | YYYY-MM-DD. Pin analysis to THIS exact opportunity instead of the auto-picked best. |
| pe_cycle | query | string ("consecutive" | "pe") | score over presidential-election-cycle years (same phase as the entry year) instead of consecutive. Default consecutive. |
| years | query | integer | lookback length (default 10). |
| period | query | string ("jan" | "feb" | "mar" | "apr" | "may" | "jun" | "jul" | "aug" | "sep" | "oct" | "nov" | "dec" | "q1" | "q2" | "q3" | "q4" | "spring" | "summer" | "fall" | "winter" | "ytd" | "year_end" | "buy_hold") | wave-viewer date-range preset; overrides entry_date/days_out. |
| reverse | query | boolean | invert the period to all-of-year-except-that-window. |
| view | query | string ("full" | "evidence" | "decision" | "table"), default full | response verbosity. 'full'/'evidence' = complete evidence card; 'decision' = lean; 'table' = compact. MCP defaults to evidence. |
| include | query | string ("chart") | include=chart attaches the canonical two-chart data and rendering specifications. MCP additionally renders them as native image content blocks. |
Response fields (AnalyzeResponse)
| Field | Type | Description |
|---|---|---|
| card | object (PatternCard) | the best setup as a PatternCard (trimmed under view='decision'; a compact row under view='table'). May carry a `note` when a bare ticker was resolved to a primary listing. |
| other_setups | array | alternate candidate setups for the same symbol (compact - no card composition) |
| view | string ("full" | "evidence" | "decision" | "table") | the verbosity applied (echoes the request) |
| disclaimer | string | exact regulatory disclaimer (envelope level) |
| as_of | string |
Example response
{
"card": {
"rank": 1,
"symbol": "AAPL",
"market": {"id": "2", "name": "S&P 500 STOCKS"},
"direction": "long",
"bias": "bullish",
"setup": {"entry_date": "2026-06-25", "entry_window": "2026-06-22 to 2026-06-28",
"hold_days": 24, "exit_date": "2026-07-19"},
"edge_score": 91,
"edge_basis": "win_rate 1.00 x sharpe 3.4 x ml_win_prob 0.71 x 10y history",
"stats": {"historical_win_rate": 1.0, "sharpe_ratio": 3.42,
"avg_return_pct": 6.05, "median_return_pct": 6.01, "years": "10"},
"ml": {"ml_score": 59.5, "ml_win_prob": 0.7087,
"pred_return_pct": 1.3736, "pred_mfe_pct": 5.8771},
"receipts": {
"years_tested": 10, "wins": 10, "losses": 0,
"historical_win_rate": 1.0, "avg_return_pct": 6.05, "median_return_pct": 6.01,
"best_year": {"year": "2016", "return_pct": 8.51},
"worst_year": {"year": "2017", "return_pct": 3.57},
"per_year": [{"year": "2025", "return_pct": 5.42, "result": "win"}],
"curve_summary": {"shape": "strengthens into the exit; seasonal index rising +13 pts over the hold",
"trend": "rising", "change_pts": 13.1,
"peak_day": 20, "trough_day": 0},
"source": "TradeWave seasonal model, 10y lookback", "as_of": "2026-05-31"
},
"next_step": {
"headline": "To act on this, here is a ready-to-place ticket:",
"order_ticket": {"side": "BUY", "symbol": "AAPL", "type": "MARKET",
"time_in_force": "DAY", "suggested_entry_date": "2026-06-25",
"suggested_exit_date": "2026-07-19",
"note": "Seasonal hold ~24 calendar days. Size to your own risk."},
"copy_text": "BUY AAPL market, day order; plan exit ~2026-07-19 (24d seasonal hold).",
"set_reminder": {"type": "seasonal_entry", "fire_on": "2026-06-24",
"message": "AAPL seasonal window opens tomorrow."},
"framing": "TradeWave gives the edge and the timing; you place the trade at your own broker."
},
"headline": "AAPL long - enter ~Jun 25, hold 24d. Won 10/10 years, avg +6.0%, Sharpe 3.4.",
"verdict": "Strong, consistent seasonal long. Seasonal shape: builds through the window (peak ~day 363), rises overall.",
"disclaimer": "Educational seasonal pattern + ML research, not personalized investment advice and not a recommendation to buy or sell. Past performance is not indicative of future results.",
"tier_notes": "ML score shown."
},
"other_setups": [
{"symbol": "AAPL", "direction": "long", "entry_date": "2026-06-24",
"hold_days": 28, "historical_win_rate": 1.0, "sharpe_ratio": 2.95,
"avg_return_pct": 6.5, "median_return_pct": 6.34, "years": "10"}
],
"as_of": "2026-05-31"
}
Response fields (Market)
| Field | Type | Description |
|---|---|---|
| id | string | |
| name | string | |
| ml_eligible | boolean | |
| in_scope | boolean | is this market in the caller's tier scope |
| pattern_detection | object (PatternDetection) | which seasonal pattern-detection paths exist for this market + an example win-rate band |
Example response
{
"markets": [
{"id": "0", "name": "DOW 30 STOCKS", "ml_eligible": true, "in_scope": true},
{"id": "2", "name": "S&P 500 STOCKS", "ml_eligible": true, "in_scope": true},
{"id": "16", "name": "CRYPTO CURRENCIES", "ml_eligible": false, "in_scope": true}
]
}
Who am I and what can I do: the key's API tier, remaining ML allowance today, rate limits, opportunity cap, and the in-scope markets (with ml_eligible per market). Powers the MCP `whoami` tool. Read-only - never consumes ML allowance. Under MCP OAuth this reflects the END USER's real tier, not the service principal.
Response fields (Me)
| Field | Type | Description |
|---|---|---|
| tier | string | The caller's tier. The purchasable API ladder is free | dev | pro | business. Bundled or web-linked principals can also see other values depending on how the caller authenticated: 'demo' (the public demo token), 'navigator' (a web Navigator sub bundled into the API, tiers.py INTERNAL_TIERS), or - under MCP OAuth - a web-mirror tier name (explorer | navigator | analyst | strategist, apiserver/auth.py _resolve_mcp). Do not treat this as a closed enum. |
| tier_name | string | |
| ml_remaining_today | integer | ML calls remaining today (null = unlimited) |
| ml_daily_limit | integer | the plan's daily ML allowance (null = unlimited) |
| opp_limit | integer | max results per opportunities/scan call on this plan |
| rate | object | the plan's rate limits |
| markets_in_scope | array | the markets this key can query, each with its ML eligibility |
| upgrade_url | string |
A full market is large (S&P 500 alone is ~150KB), so the list pages: `prefix` filters by ticker prefix (case-insensitive) and `limit` caps the rows returned. total/matched/count make any truncation explicit, never silent (a `note` names how many matching rows were cut).
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| market_idrequired | path | string | permanent resource key '0'..'16' |
| prefix | query | string | ticker prefix filter, case-insensitive (e.g. 'AA' matches AAPL, AAL) |
| limit | query | integer | max rows returned (after the prefix filter); omitted = all matching rows |
Response fields (Symbol)
| Field | Type | Description |
|---|---|---|
| symbol | string | |
| name | string |
Example response
{
"symbols": [
{"symbol": "AAPL", "name": "Apple Inc"},
{"symbol": "MSFT", "name": "Microsoft Corp"}
]
}
The core primitive. Returns ranked seasonal trade setups for ONE market at a single entry_date (echoed in the response). It does NOT widen across a date window - /scan owns windows. 'from' is accepted; 'to' is accepted but does NOT widen the search (window_supported is false). ML fields populate up to the caller's daily ML allowance on ML-eligible markets (ids 0-4, 11); the ml block here keeps the legacy names {ml_score, win_prob, pred_return, pred_mfe}. ml_remaining_today reflects how many ML calls remain today (null = unlimited).
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| marketrequired | query | string | market id '0'..'16' |
| from | query | string | entry_date to evaluate; defaults to today |
| to | query | string | ACCEPTED but ignored - does not widen the window (window_supported:false). Use /scan for windows. |
| direction | query | string ("long" | "short") | |
| min_win_rate | query | number | filter on the REAL historical win_rate (share of profitable years), not the ML win_prob. Rows are enriched per symbol from ChartData4 'Percent Profitable', capped at the first 50 rows; rows past the cap (unknown win_rate) are excluded when this filter is set. |
| min_days | query | integer | minimum pattern length (holding period) in calendar days, e.g. 10 |
| max_days | query | integer | maximum pattern length (holding period) in calendar days, e.g. 90 |
| min_avg_return | query | number | minimum average seasonal profit in PERCENT (e.g. 5 means >= 5%) |
| min_median_return | query | number | minimum median seasonal profit in PERCENT |
| min_sharpe | query | number | minimum Sharpe ratio, e.g. 1.5 |
| pe_cycle | query | string ("consecutive" | "pe"), default consecutive | presidential election cycle mode: 'consecutive' (default) or 'pe' (the current cycle position) |
| years | query | integer, default 10 | pattern lookback in years (5-98, data-dependent) |
| min_winning_years | query | integer | of those `years`, the minimum WINNING years (the win-rate floor; year2). DEFAULTS to ~90% of `years`. Single market: validated against that market's DETECTION BAND (~75-90%+, market-specific); an out-of-band value (e.g. 20-9) returns 400 with the valid range. |
| limit | query | integer, default 25 | tier-capped (free 3, dev 100, pro 1000, business 5000) |
Response fields (OpportunityList)
| Field | Type | Description |
|---|---|---|
| entry_date | string | the single entry_date evaluated (echoes 'from', defaults to today) |
| window_supported | boolean | always false here - 'to' does not widen the window; /scan owns windows |
| evaluated_count | integer | how many candidate rows were enriched with win_rate (the enrichment cap, e.g. 50) |
| enrichment_capped | boolean | true when more rows existed than were win_rate-enriched (rows past the cap have unknown win_rate) |
| ml_remaining_today | integer | ML calls remaining today for this key (null = unlimited; 0 = daily limit reached). Free 5/day, Dev 100/day, Pro/Business unlimited. |
| opportunities | array | |
| disclaimer | string | exact regulatory disclaimer (this is a pattern-bearing response) |
Example response
{
"entry_date": "2026-05-31",
"window_supported": false,
"evaluated_count": 50,
"enrichment_capped": true,
"opportunities": [
{
"symbol": "DOV", "market": "2",
"direction": "long","entry_date": "2026-06-01",
"days_out": 246, "sharpe_ratio": 3.31,
"avg_profit_pct": 18.49, "median_profit_pct": 18.4,
"win_rate": 1.0, "years": "10",
"ml": null
},
{
"symbol": "NDSN", "market": "2",
"direction": "long","entry_date": "2026-05-31",
"days_out": 82, "sharpe_ratio": 2.66,
"avg_profit_pct": 5.68, "median_profit_pct": 6.04,
"win_rate": 1.0, "years": "10",
"ml": {"ml_score": 29.0, "win_prob": 0.6665,
"pred_return": 2.2999, "pred_mfe": 8.5967}
}
]
}
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| symbolrequired | path | string | |
| market | query | string | Market id '0'..'16'. Required only when your key has access to more than one market; a single-market key (the free/demo/navigator case) may omit it - the one in-scope market is used automatically. |
| pe_cycle | query | string ("consecutive" | "pe"), default consecutive | presidential election cycle: 'consecutive' (default) or 'pe' (current cycle position) |
| min_days | query | integer | minimum pattern length in days |
| max_days | query | integer | maximum pattern length in days |
| min_avg_return | query | number | minimum average seasonal profit, PERCENT |
| min_median_return | query | number | minimum median seasonal profit, PERCENT |
| min_sharpe | query | number | minimum Sharpe ratio |
| years | query | integer, default 10 | pattern lookback in years (5-98, data-dependent) |
| min_winning_years | query | integer | of those `years`, the minimum WINNING years (the win-rate floor; year2). DEFAULTS to ~90% of `years`; validated against the market's DETECTION BAND (~75-90%+, market-specific); an out-of-band value returns 400 with the valid range. Per-symbol patterns exist for market ids 0,1,2,7,9 only; other markets return 400. |
Response fields (Opportunity)
| Field | Type | Description |
|---|---|---|
| symbol | string | |
| market | string | |
| direction | string ("long" | "short") | long or short - which side of the seasonal pattern the stats assume |
| entry_date | string | |
| days_out | integer | holding period in days (internal: daysOut) |
| sharpe_ratio | number | |
| avg_profit_pct | number | |
| median_profit_pct | number | |
| win_rate | number | REAL historical win rate, 0..1: share of years the seasonal window finished profitable (any positive year; no threshold), from ChartData4 'Percent Profitable'. Distinct from ml.win_prob. May be null on the bulk list past the per-row enrichment cap. |
| years | string | lookback window label, stays a string |
| ml | object (MLScore) | ML block. Available on every tier, metered per day (free 5/day, Dev 100/day, Pro/Business unlimited); null only when the daily ML allowance is spent or the market is not ML-eligible (ids 0-4, 11). Legacy names (win_prob, pred_return, pred_mfe). |
Example response
{
"opportunities": [
{
"symbol": "AAPL", "market": "2",
"direction": "long","entry_date": "2026-06-01",
"days_out": 22, "sharpe_ratio": 1.84,
"avg_profit_pct": 3.12, "median_profit_pct": 2.88,
"win_rate": 0.78, "years": "10",
"ml": null
}
]
}
Same data as GET /opportunities/{symbol} - one security's ranked seasonal patterns throughout the year (the wave-viewer pattern dropdown). Both endpoints are served by the same handler, so their parameters are identical (shared $refs below) and cannot drift apart.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| symbolrequired | path | string | |
| market | query | string | Market id '0'..'16'. Required only when your key has access to more than one market; a single-market key (the free/demo/navigator case) may omit it - the one in-scope market is used automatically. |
| pe_cycle | query | string ("consecutive" | "pe"), default consecutive | presidential election cycle: 'consecutive' (default) or 'pe' (current cycle position) |
| min_days | query | integer | minimum pattern length in days |
| max_days | query | integer | maximum pattern length in days |
| min_avg_return | query | number | minimum average seasonal profit, PERCENT |
| min_median_return | query | number | minimum median seasonal profit, PERCENT |
| min_sharpe | query | number | minimum Sharpe ratio |
| years | query | integer, default 10 | pattern lookback in years (5-98, data-dependent) |
| min_winning_years | query | integer | of those `years`, the minimum WINNING years (the win-rate floor; year2). DEFAULTS to ~90% of `years`; validated against the market's DETECTION BAND (~75-90%+, market-specific); an out-of-band value returns 400 with the valid range. Per-symbol patterns exist for market ids 0,1,2,7,9 only; other markets return 400. |
Response fields (Opportunity)
| Field | Type | Description |
|---|---|---|
| symbol | string | |
| market | string | |
| direction | string ("long" | "short") | long or short - which side of the seasonal pattern the stats assume |
| entry_date | string | |
| days_out | integer | holding period in days (internal: daysOut) |
| sharpe_ratio | number | |
| avg_profit_pct | number | |
| median_profit_pct | number | |
| win_rate | number | REAL historical win rate, 0..1: share of years the seasonal window finished profitable (any positive year; no threshold), from ChartData4 'Percent Profitable'. Distinct from ml.win_prob. May be null on the bulk list past the per-row enrichment cap. |
| years | string | lookback window label, stays a string |
| ml | object (MLScore) | ML block. Available on every tier, metered per day (free 5/day, Dev 100/day, Pro/Business unlimited); null only when the daily ML allowance is spent or the market is not ML-eligible (ids 0-4, 11). Legacy names (win_prob, pred_return, pred_mfe). |
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| market_idrequired | path | string | permanent resource key '0'..'16' |
| symbolrequired | path | string | |
| pe_cycle | query | string ("consecutive" | "pe" | "pe0" | "pe1" | "pe2" | "pe3"), default consecutive | presidential cycle: consecutive (default), pe (current position), or a specific position pe0..pe3 (PE+0 = election years, PE+2 = current in 2026) |
| years | query | integer, default 10 | lookback count (years, or number of cycle occurrences when pe_cycle is a pe* value) |
| days_out | query | integer, default 30 | |
| period | query | string ("jan" | "feb" | "mar" | "apr" | "may" | "jun" | "jul" | "aug" | "sep" | "oct" | "nov" | "dec" | "q1" | "q2" | "q3" | "q4" | "spring" | "summer" | "fall" | "winter" | "ytd" | "year_end" | "buy_hold") | date-range PRESET (overrides entry_date/days_out): a month, quarter, season, ytd, year_end, or buy_hold |
| reverse | query | boolean | use the COMPLEMENT of the window (e.g. period=mar&reverse=true = all year except March). buy_hold cannot be reversed. |
Response fields (Pattern)
| Field | Type | Description |
|---|---|---|
| symbol | string | |
| market | string | |
| win_rate | number | REAL historical win rate, 0..1: share of profitable years (no threshold), from 'Percent Profitable'. Also present inside stats as percent_profitable. |
| stats | object | Publishable percent-only subset of the ChartData4 stats; price/volume levels (52W high/low, SMA, avg volume) are excluded. |
| disclaimer | string | exact regulatory disclaimer (this is a pattern-bearing response) |
Example response
{
"disclaimer": "Educational seasonal pattern + ML research, not personalized investment advice and not a recommendation to buy or sell. Past performance is not indicative of future results.",
"market": "2",
"symbol": "AAPL",
"win_rate": 0.8,
"stats": {
"trade_direction": "long",
"percent_profitable": "80%",
"num_winners": "8", "num_losers": "2",
"avg_profit": "10.66%", "avg_loss": "-4.99%",
"median_profit": "9.05%",
"sharpe_ratio": "0.85", "sharpe_ratio_mfe": "1.56",
"annualized_return": "7.22%", "cumulative_return": "100%",
"std_dev": "8.5%", "return_1m": "10.49%", "avg_profit_all": "8%",
"trend_long": 79, "trend_long_prev": 65,
"trend_short": 0, "trend_short_prev": 5
}
}
The seasonal 'trendchart' as DATA (points to plot, not an image), so callers can chart it or agents can reason over the shape. Returns the appserver's consolidated_seasonal_chart2 curve: a year-averaged, high/low-normalized 0-100 seasonal index, one point per day over the annual cycle, plus the publishable percentage stats and the historical win_rate. The raw price series (the app's 'price' chart mode) is intentionally NOT exposed.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| marketrequired | query | string | |
| symbolrequired | query | string | |
| entry_date | query | string | |
| days_out | query | integer | |
| direction | query | string ("long" | "short") | |
| years | query | string | lookback window, stays a string |
| pe_cycle | query | string ("consecutive" | "pe" | "pe0" | "pe1" | "pe2" | "pe3"), default consecutive | presidential cycle for the curve: consecutive (default), pe (current position), or a specific position pe0..pe3 |
| period | query | string ("jan" | "feb" | "mar" | "apr" | "may" | "jun" | "jul" | "aug" | "sep" | "oct" | "nov" | "dec" | "q1" | "q2" | "q3" | "q4" | "spring" | "summer" | "fall" | "winter" | "ytd" | "year_end" | "buy_hold") | date-range PRESET (overrides entry_date/days_out): a month, quarter, season, ytd (year to date), year_end (today to year end), or buy_hold (Jan 1 to Jan 1, full year) |
| reverse | query | boolean | use the COMPLEMENT of the window - everything except it (e.g. period=mar&reverse=true = all year except March). A full-year (buy_hold) range cannot be reversed (400). |
Response fields (SeasonalChart)
| Field | Type | Description |
|---|---|---|
| symbol | string | |
| market | string | |
| direction | string ("long" | "short") | |
| entry_date | string | start of the annual cycle (chart_start_date) |
| days_out | integer | echoed for parity; does not change the full-cycle curve |
| years | string | lookback window label, stays a string |
| win_rate | number | REAL historical win rate, 0..1, from ChartData4 'Percent Profitable' (same as /patterns). |
| seasonal_curve | array | the smooth seasonal line - year-averaged 0-100 normalized index, one point per calendar day of the cycle |
| stats | object | same publishable percent-only stat block as /patterns |
| per_year_bars | array | per-year bars for the Trend Chart - each completed year's trade return plus its favorable/adverse excursion band, returned alongside the curve (one-call curve+bars). Direction-aware, percentages only. |
| disclaimer | string | exact regulatory disclaimer (this is a pattern-bearing response) |
Example response
{
"symbol": "AAPL", "market": "2",
"direction": "long","entry_date": "2026-06-01",
"days_out": 22, "years": "10",
"seasonal_curve": [
{"date": "2026-06-01", "index": 41.2},
{"date": "2026-06-02", "index": 42.8},
{"date": "2026-06-23", "index": 58.6}
],
"stats": {"avg_profit_pct": 3.12, "win_rate": 0.78},
"win_rate": 0.78, "years": "10"
}
Available on every tier, metered per day (free 5/day, dev 100/day, pro/business unlimited). ML-eligible markets only (ids 0-4, 11). Scores up to the daily allowance and returns granted (how many were actually scored) and ml_remaining_today (integer or null for unlimited). Items beyond the granted allowance appear in scores[] unscored with note:"daily ML limit reached - upgrade for unlimited". When the allowance is already fully spent the response is a 200 with requires:"upgrade" and reason:"ml_daily_limit" instead of a scores array.
Request body - array of opportunity identifiers
| Field | Type | Description |
|---|---|---|
| symbolrequired | string | |
| daterequired | string | |
| days_outrequired | integer | |
| directionrequired | string ("long" | "short") |
Example response
{
"granted": 1,
"ml_remaining_today": null,
"scores": [
{
"symbol": "AAPL",
"date": "2026-06-01",
"direction": "long",
"days_out": 20,
"ml_score": 95.1,
"win_prob": 0.8452,
"pred_return": 4.0913,
"pred_mfe": 7.4427
}
]
}
The ML-selected featured opportunity, returned as a full PatternCard. The card's receipts ALSO carry live_track_record (the forward-tested record of past picks - made in advance, scored later), the strongest receipt. featured_date is the date the pick was made; track_record echoes the live record at the top level too.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| view | query | string ("full" | "evidence" | "decision" | "table"), default full | response verbosity: 'full' (default), 'evidence' (full rank-1 card), 'decision' (lean), or 'table'. |
| include | query | string ("chart") | include=chart attaches a `chart` block (Trend Chart curve + per-year bars) to the pick card. |
Response fields (DailyPickResponse)
| Field | Type | Description |
|---|---|---|
| card | object (PatternCard) | the pick as a PatternCard (trimmed under view='decision'); receipts.live_track_record carries the live record |
| featured_date | string | the date the pick was made |
| track_record | object (TrackRecordSummary) | the live forward-tested record (echoed at top level) |
| view | string ("full" | "evidence" | "decision" | "table") | the verbosity applied (echoes the request) |
| disclaimer | string | exact regulatory disclaimer (envelope level) |
| as_of | string |
Example response
{
"card": {
"rank": 1,
"symbol": "NVDA",
"market": {"id": "2", "name": "S&P 500 STOCKS"},
"direction": "long",
"bias": "bullish",
"setup": {"entry_date": "2026-05-08", "entry_window": "2026-05-05 to 2026-05-11",
"hold_days": 30, "exit_date": "2026-06-07"},
"edge_score": 85,
"edge_basis": "win_rate 0.82 x sharpe 2.8 x ml_win_prob 0.85 x 11y history",
"stats": {"historical_win_rate": 0.82, "sharpe_ratio": 2.84,
"avg_return_pct": 23.79, "median_return_pct": 23.51, "years": "10"},
"ml": {"ml_score": 95.6, "ml_win_prob": 0.8452,
"pred_return_pct": 6.8239, "pred_mfe_pct": 14.4871},
"receipts": {
"years_tested": 11, "wins": 9, "losses": 2,
"historical_win_rate": 0.82, "avg_return_pct": 23.79, "median_return_pct": 23.51,
"best_year": {"year": "2017", "return_pct": 45.25},
"worst_year": {"year": "2019", "return_pct": -16.24},
"per_year": [{"year": "2019", "return_pct": -16.24, "result": "loss"}],
"curve_summary": {"shape": "builds through the window (peak ~day 364), rises overall",
"peak_day": 364, "trough_day": 0},
"source": "TradeWave seasonal model, 10y lookback", "as_of": "2026-05-31",
"live_track_record": {"count": 10, "win_count": 7, "win_rate": 0.7,
"avg_return_pct": 6.69,
"note": "Live forward-tested record of past daily picks (made in advance, scored later)."}
},
"next_step": {
"headline": "To act on this, here is a ready-to-place ticket:",
"order_ticket": {"side": "BUY", "symbol": "NVDA", "type": "MARKET",
"time_in_force": "DAY", "suggested_entry_date": "2026-05-08",
"suggested_exit_date": "2026-06-07",
"note": "Seasonal hold ~30 calendar days. Size to your own risk."},
"copy_text": "BUY NVDA market, day order; plan exit ~2026-06-07 (30d seasonal hold).",
"set_reminder": {"type": "seasonal_entry", "fire_on": "2026-05-07",
"message": "NVDA seasonal window opens tomorrow."},
"framing": "TradeWave gives the edge and the timing; you place the trade at your own broker."
},
"headline": "NVDA long - enter ~May 8, hold 30d. Won 9/11 years, avg +23.8%, Sharpe 2.8.",
"verdict": "Strong, consistent seasonal long. Seasonal shape: builds through the window (peak ~day 364), rises overall.",
"disclaimer": "Educational seasonal pattern + ML research, not personalized investment advice and not a recommendation to buy or sell. Past performance is not indicative of future results.",
"tier_notes": "ML score shown."
},
"featured_date": "2026-05-08",
"track_record": {"count": 10, "win_count": 7, "win_rate": 0.7,
"avg_return_pct": 6.69,
"note": "Live forward-tested record of past daily picks (made in advance, scored later)."},
"as_of": "2026-05-31"
}
The verifiable, time-stamped performance record. Free-tier accessible (the marketing hook).
Response fields (TrackRecord)
| Field | Type | Description |
|---|---|---|
| summary | object | |
| picks | array | |
| disclaimer | string | exact regulatory disclaimer (performance record - 'past performance is not indicative...') |
Example response
{
"summary": {
"count": 312, "win_count": 249,
"win_rate": 0.798, "avg_return_pct": 3.1
},
"picks": [
{"symbol": "NVDA", "featured_date": "2026-05-27",
"return_pct": null, "result": "open"},
{"symbol": "AAPL", "featured_date": "2026-05-06",
"return_pct": 4.5, "result": "win"}
]
}
OpenAPI spec
The machine-readable contract lives at /api/openapi.yaml in the repository. You can point any OpenAPI-compatible tool (Redoc, Swagger UI, Insomnia, Postman) at it.
Error shape
All errors follow the same envelope:
{
"error": {
"code": "rate_limited",
"message": "You have exceeded 10 requests per minute."
}
}
See Rate Limits & Errors for the full list of codes.