API Reference
All endpoints are under https://api.tradewave.ai/v1. Authenticate with Authorization: Bearer <key> or X-API-Key: <key>. Allowed browser origins can read the rate-limit and Retry-After headers.
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. Scans verify a bounded 50-row candidate pool before applying return/Sharpe filters, final ranking, and the requested result limit. ML scores omitted from the returned page are refunded. evaluated_count + enrichment_capped are surfaced, and any partial market failure is explicit in market_failures. Unavailable evidence cannot satisfy minimum return, Sharpe, win-rate or history filters. evidence_failures counts failed candidate records even when filtering removes them; an incomplete scan must not be interpreted as verified absence. Discovery starts must be in the current US market year, or tomorrow at the New Year boundary. Other years return 400; use pinned /analyze for exact-window research.
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 |
| evidence_failures | integer | candidate evidence fetches that failed, including candidates excluded by filters; nonzero means the scan is incomplete |
| 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. Pinned analysis bypasses pattern detection and evaluates the exact requested hold. Without a pinned window, best-setup discovery uses the market's detection band.
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 | inclusive calendar-day count; entry day is day 1 and end = entry_date + (days_out - 1). 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 Apps hosts can render the evidence widget; other hosts receive text evidence and the exact viewer link. |
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). The first 50 duration-matching candidates receive completed historical evidence. Win rate, average, median and Sharpe use that same direction and lookback; statistical filters and final Sharpe ordering run after this refresh. Rows past the cap have null historical fields and evidence_status=not_evaluated. Outages use evidence_status=unavailable; a valid empty cohort uses no_data. Entry dates must be in the current US market year, or tomorrow at the New Year boundary. Unsupported years return 400 rather than substitute current-year patterns.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| marketrequired | query | string | market id '0'..'16' |
| from | query | string | entry_date to evaluate in the current US market year (or tomorrow); defaults to today. Other years require exact-window analysis. |
| 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 attempted for completed historical evidence (maximum 50) |
| enrichment_capped | boolean | true when rows beyond the evidence cap have null historical statistics |
| 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 | inclusive holding period in calendar days; entry day is day 1 and end = entry_date + (days_out - 1) |
| 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 |
| years_tested | integer | completed observations in this exact historical cohort; null when not verified |
| evidence_status | string ("verified" | "no_data" | "unavailable" | "not_evaluated") | historical evidence state; unverified rows have null historical statistics |
| 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 | inclusive holding period in calendar days; entry day is day 1 and end = entry_date + (days_out - 1) |
| 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 |
| years_tested | integer | completed observations in this exact historical cohort; null when not verified |
| evidence_status | string ("verified" | "no_data" | "unavailable" | "not_evaluated") | historical evidence state; unverified rows have null historical statistics |
| 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 | inclusive calendar-day count; entry day is day 1 |
| 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 | inclusive calendar-day count; entry day is day 1 |
| 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 | inclusive calendar-day count (entry day is day 1); echoed for parity and 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. Exact-window ML supports 10-90 calendar days. Unsupported windows are excluded before reserving allowance and remain unscored in their original positions with a duration note. A batch containing only unsupported windows returns granted:0 and a scores array without a quota-exhaustion upgrade prompt. Current-condition scores also require an entry date from today through five calendar days ahead in America/New_York. After-entry and farther-ahead rows remain unscored with an explicit date note and do not reserve allowance.
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 app's Symbol Comparison: every symbol is measured on one setup (entry_date + days_out, or a period preset; years; pe_cycle; direction) and restricted to the completed years all symbols share (latest `years` of them, minimum 5 for can_compare). Returns per-symbol metrics (average, median, profitable %, best/worst year, average MFE/MAE, deepest drop, Sharpe, cumulative), a same-years buy-and-hold benchmark, yearly results, and deterministic findings. Percentages only.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| symbolsrequired | query | string | comma-separated, 2-10 distinct tickers |
| market | query | string | |
| entry_date | query | string | |
| days_out | query | integer | |
| period | query | string | |
| years | query | integer, default 10 | |
| pe_cycle | query | string | |
| direction | query | string ("long" | "short") |
The app's Portfolio Scenarios math in percentages for a HYPOTHETICAL basket of 1-15 symbols with weight_pct (normalized to 100). Every symbol is measured from one start date over each horizon (30, 60, 90, eoy, custom up to 366 days / end_date) and the same completed years. Returns average/median result, best/worst year, years up/down, yearly results with contributions, optional same-years benchmark, and summary lines. Accepts only symbol, weight_pct, market and direction - never holdings, share counts, dollar amounts, cost basis or P&L.
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 (TrackRecordSummary) | |
| 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.
Temporary database, cache, or upstream unavailability returns a safe 503 response.
Infrastructure failures include Retry-After. Authenticated
failures retain rate-limit headers. Retries and parallel market work share one upstream
time budget; a retry does not restart that budget.