API Reference

All 13 endpoints, generated from the OpenAPI contract.

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

GET /v1/scan Scan in-scope markets for the best seasonal setups (ranked PatternCards) All tiers

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

NameInTypeDescription
marketsquerystringcsv 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.
windowquerystring, 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'.
directionquerystring ("long" | "short")
min_win_ratequerynumberfilter on historical_win_rate (share of profitable years), NOT ml_win_prob. Rows past the enrichment cap are excluded when set.
min_yearsqueryintegertrust filter - require years_tested >= N
min_daysqueryintegerminimum pattern length (holding period) in calendar days, e.g. 10
max_daysqueryintegermaximum pattern length (holding period) in calendar days, e.g. 90
min_avg_returnquerynumberminimum average seasonal profit in PERCENT (e.g. 5 means >= 5%); matches stats.avg_return_pct
min_median_returnquerynumberminimum median seasonal profit in PERCENT; matches stats.median_return_pct
min_sharpequerynumberminimum Sharpe ratio, e.g. 1.5; matches stats.sharpe_ratio
pe_cyclequerystring ("consecutive" | "pe"), default consecutivepresidential election cycle mode: 'consecutive' (default, consecutive years) or 'pe' (the current presidential-cycle position only)
yearsqueryinteger, default 10pattern 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_yearsqueryintegerof 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_byquerystring ("edge" | "win_rate" | "sharpe" | "ml" | "avg_return"), default sharperanking key; default sharpe (Sharpe ratio)
limitqueryinteger, default 25tier-capped (free 3, dev 100, pro 1000, business 5000)
viewquerystring ("full" | "evidence" | "decision" | "table"), default fullresponse verbosity. 'full' (API default) = complete cards; 'evidence' = full rank-1 winner + lean runners; 'decision' = lean cards; 'table' = compact rows. MCP discovery defaults to evidence.
includequerystring ("chart")include=chart attaches the two-chart evidence data/specifications to the rank-1 winner only.

Response fields (ScanResponse)

FieldTypeDescription
generated_atstring
summarystringthe 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
windowstringthe resolved window (e.g. 'now')
markets_scannedarraythe requested in-scope market ids; check market_failures before treating the scan as complete
market_failuresarraymarket ids whose upstream request failed and whose results were omitted; normally empty
rank_bystring ("edge" | "win_rate" | "sharpe" | "ml" | "avg_return")
viewstring ("full" | "evidence" | "decision" | "table")the verbosity applied (echoes the request)
lookbackobjectthe resolved detection knobs (year1, year2)
countintegernumber of cards returned
evaluated_countintegercandidates considered before slicing/enrichment
enrichment_cappedbooleantrue when only the leading requested/bounded candidate depth was fully receipt-enriched
capped_by_planbooleantrue 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_evaluatedstring'shown of evaluated' as a display string
upgrade_urlstringpresent only when capped_by_plan is true
ml_remaining_todayintegerML calls remaining today for this key (null = unlimited; 0 = daily limit reached). Free 5/day, Dev 100/day, Pro/Business unlimited.
opportunitiesarrayranked 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_notestringpresent 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.
disclaimerstringexact 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."
    }
  ]
}
GET /v1/analyze/{symbol} Deep-dive one symbol into a single rich PatternCard + alternate setups All tiers

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

NameInTypeDescription
symbolrequiredpathstring
marketquerystringpermanent resource key '0'..'16'. Optional - resolved if the symbol is unique.
directionquerystring ("long" | "short")
days_outqueryintegerholding period in days (internal: daysOut). With entry_date, pins the exact window.
entry_datequerystringYYYY-MM-DD. Pin analysis to THIS exact opportunity instead of the auto-picked best.
pe_cyclequerystring ("consecutive" | "pe")score over presidential-election-cycle years (same phase as the entry year) instead of consecutive. Default consecutive.
yearsqueryintegerlookback length (default 10).
periodquerystring ("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.
reversequerybooleaninvert the period to all-of-year-except-that-window.
viewquerystring ("full" | "evidence" | "decision" | "table"), default fullresponse verbosity. 'full'/'evidence' = complete evidence card; 'decision' = lean; 'table' = compact. MCP defaults to evidence.
includequerystring ("chart")include=chart attaches the canonical two-chart data and rendering specifications. MCP additionally renders them as native image content blocks.

Response fields (AnalyzeResponse)

FieldTypeDescription
cardobject (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_setupsarrayalternate candidate setups for the same symbol (compact - no card composition)
viewstring ("full" | "evidence" | "decision" | "table")the verbosity applied (echoes the request)
disclaimerstringexact regulatory disclaimer (envelope level)
as_ofstring

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"
}
GET /v1/markets List the 15 active markets (ids 0-13, 16) and the caller's access scope + pattern coverage All tiers

Response fields (Market)

FieldTypeDescription
idstring
namestring
ml_eligibleboolean
in_scopebooleanis this market in the caller's tier scope
pattern_detectionobject (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}
  ]
}
GET /v1/me Identity + capabilities for the caller's key (plan, quotas, market scope) All tiers

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)

FieldTypeDescription
tierstringThe 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_namestring
ml_remaining_todayintegerML calls remaining today (null = unlimited)
ml_daily_limitintegerthe plan's daily ML allowance (null = unlimited)
opp_limitintegermax results per opportunities/scan call on this plan
rateobjectthe plan's rate limits
markets_in_scopearraythe markets this key can query, each with its ML eligibility
upgrade_urlstring
GET /v1/markets/{market_id}/symbols List symbols in a market (pageable) All tiers

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

NameInTypeDescription
market_idrequiredpathstringpermanent resource key '0'..'16'
prefixquerystringticker prefix filter, case-insensitive (e.g. 'AA' matches AAPL, AAL)
limitqueryintegermax rows returned (after the prefix filter); omitted = all matching rows

Response fields (Symbol)

FieldTypeDescription
symbolstring
namestring

Example response

{
  "symbols": [
    {"symbol": "AAPL", "name": "Apple Inc"},
    {"symbol": "MSFT", "name": "Microsoft Corp"}
  ]
}
GET /v1/opportunities Ranked seasonal opportunities (single entry-date primitive) All tiers

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

NameInTypeDescription
marketrequiredquerystringmarket id '0'..'16'
fromquerystringentry_date to evaluate; defaults to today
toquerystringACCEPTED but ignored - does not widen the window (window_supported:false). Use /scan for windows.
directionquerystring ("long" | "short")
min_win_ratequerynumberfilter 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_daysqueryintegerminimum pattern length (holding period) in calendar days, e.g. 10
max_daysqueryintegermaximum pattern length (holding period) in calendar days, e.g. 90
min_avg_returnquerynumberminimum average seasonal profit in PERCENT (e.g. 5 means >= 5%)
min_median_returnquerynumberminimum median seasonal profit in PERCENT
min_sharpequerynumberminimum Sharpe ratio, e.g. 1.5
pe_cyclequerystring ("consecutive" | "pe"), default consecutivepresidential election cycle mode: 'consecutive' (default) or 'pe' (the current cycle position)
yearsqueryinteger, default 10pattern lookback in years (5-98, data-dependent)
min_winning_yearsqueryintegerof 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.
limitqueryinteger, default 25tier-capped (free 3, dev 100, pro 1000, business 5000)

Response fields (OpportunityList)

FieldTypeDescription
entry_datestringthe single entry_date evaluated (echoes 'from', defaults to today)
window_supportedbooleanalways false here - 'to' does not widen the window; /scan owns windows
evaluated_countintegerhow many candidate rows were enriched with win_rate (the enrichment cap, e.g. 50)
enrichment_cappedbooleantrue when more rows existed than were win_rate-enriched (rows past the cap have unknown win_rate)
ml_remaining_todayintegerML calls remaining today for this key (null = unlimited; 0 = daily limit reached). Free 5/day, Dev 100/day, Pro/Business unlimited.
opportunitiesarray
disclaimerstringexact 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}
    }
  ]
}
GET /v1/opportunities/{symbol} A symbol's top seasonal patterns across the year, ranked by Sharpe (the wave-viewer pattern list) All tiers

Parameters

NameInTypeDescription
symbolrequiredpathstring
marketquerystringMarket 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_cyclequerystring ("consecutive" | "pe"), default consecutivepresidential election cycle: 'consecutive' (default) or 'pe' (current cycle position)
min_daysqueryintegerminimum pattern length in days
max_daysqueryintegermaximum pattern length in days
min_avg_returnquerynumberminimum average seasonal profit, PERCENT
min_median_returnquerynumberminimum median seasonal profit, PERCENT
min_sharpequerynumberminimum Sharpe ratio
yearsqueryinteger, default 10pattern lookback in years (5-98, data-dependent)
min_winning_yearsqueryintegerof 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)

FieldTypeDescription
symbolstring
marketstring
directionstring ("long" | "short")long or short - which side of the seasonal pattern the stats assume
entry_datestring
days_outintegerholding period in days (internal: daysOut)
sharpe_rationumber
avg_profit_pctnumber
median_profit_pctnumber
win_ratenumberREAL 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.
yearsstringlookback window label, stays a string
mlobject (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
    }
  ]
}
GET /v1/securities/{symbol}/patterns A symbol's top seasonal patterns across the year, ranked by Sharpe (clearly-named alias) All tiers

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

NameInTypeDescription
symbolrequiredpathstring
marketquerystringMarket 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_cyclequerystring ("consecutive" | "pe"), default consecutivepresidential election cycle: 'consecutive' (default) or 'pe' (current cycle position)
min_daysqueryintegerminimum pattern length in days
max_daysqueryintegermaximum pattern length in days
min_avg_returnquerynumberminimum average seasonal profit, PERCENT
min_median_returnquerynumberminimum median seasonal profit, PERCENT
min_sharpequerynumberminimum Sharpe ratio
yearsqueryinteger, default 10pattern lookback in years (5-98, data-dependent)
min_winning_yearsqueryintegerof 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)

FieldTypeDescription
symbolstring
marketstring
directionstring ("long" | "short")long or short - which side of the seasonal pattern the stats assume
entry_datestring
days_outintegerholding period in days (internal: daysOut)
sharpe_rationumber
avg_profit_pctnumber
median_profit_pctnumber
win_ratenumberREAL 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.
yearsstringlookback window label, stays a string
mlobject (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).
GET /v1/patterns/{market_id}/{symbol} Seasonal pattern stats for a symbol (aggregate stats, no price series) All tiers

Parameters

NameInTypeDescription
market_idrequiredpathstringpermanent resource key '0'..'16'
symbolrequiredpathstring
pe_cyclequerystring ("consecutive" | "pe" | "pe0" | "pe1" | "pe2" | "pe3"), default consecutivepresidential cycle: consecutive (default), pe (current position), or a specific position pe0..pe3 (PE+0 = election years, PE+2 = current in 2026)
yearsqueryinteger, default 10lookback count (years, or number of cycle occurrences when pe_cycle is a pe* value)
days_outqueryinteger, default 30
periodquerystring ("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
reversequerybooleanuse the COMPLEMENT of the window (e.g. period=mar&reverse=true = all year except March). buy_hold cannot be reversed.

Response fields (Pattern)

FieldTypeDescription
symbolstring
marketstring
win_ratenumberREAL historical win rate, 0..1: share of profitable years (no threshold), from 'Percent Profitable'. Also present inside stats as percent_profitable.
statsobjectPublishable percent-only subset of the ChartData4 stats; price/volume levels (52W high/low, SMA, avg volume) are excluded.
disclaimerstringexact 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
  }
}
GET /v1/seasonal-chart Seasonal trendchart data for a symbol (normalized 0-100 curve, no price series) All tiers

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

NameInTypeDescription
marketrequiredquerystring
symbolrequiredquerystring
entry_datequerystring
days_outqueryinteger
directionquerystring ("long" | "short")
yearsquerystringlookback window, stays a string
pe_cyclequerystring ("consecutive" | "pe" | "pe0" | "pe1" | "pe2" | "pe3"), default consecutivepresidential cycle for the curve: consecutive (default), pe (current position), or a specific position pe0..pe3
periodquerystring ("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)
reversequerybooleanuse 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)

FieldTypeDescription
symbolstring
marketstring
directionstring ("long" | "short")
entry_datestringstart of the annual cycle (chart_start_date)
days_outintegerechoed for parity; does not change the full-cycle curve
yearsstringlookback window label, stays a string
win_ratenumberREAL historical win rate, 0..1, from ChartData4 'Percent Profitable' (same as /patterns).
seasonal_curvearraythe smooth seasonal line - year-averaged 0-100 normalized index, one point per calendar day of the cycle
statsobjectsame publishable percent-only stat block as /patterns
per_year_barsarrayper-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.
disclaimerstringexact 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"
}
POST /v1/score ML scores for opportunities (all tiers, metered per day) All tiers

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

FieldTypeDescription
symbolrequiredstring
daterequiredstring
days_outrequiredinteger
directionrequiredstring ("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
    }
  ]
}
GET /v1/daily-pick Today's AI pick as a PatternCard, with its live track record All tiers

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

NameInTypeDescription
viewquerystring ("full" | "evidence" | "decision" | "table"), default fullresponse verbosity: 'full' (default), 'evidence' (full rank-1 card), 'decision' (lean), or 'table'.
includequerystring ("chart")include=chart attaches a `chart` block (Trend Chart curve + per-year bars) to the pick card.

Response fields (DailyPickResponse)

FieldTypeDescription
cardobject (PatternCard)the pick as a PatternCard (trimmed under view='decision'); receipts.live_track_record carries the live record
featured_datestringthe date the pick was made
track_recordobject (TrackRecordSummary)the live forward-tested record (echoed at top level)
viewstring ("full" | "evidence" | "decision" | "table")the verbosity applied (echoes the request)
disclaimerstringexact regulatory disclaimer (envelope level)
as_ofstring

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"
}
GET /v1/daily-pick/track-record Historical track record of past daily picks (realized win/loss) All tiers

The verifiable, time-stamped performance record. Free-tier accessible (the marketing hook).

Response fields (TrackRecord)

FieldTypeDescription
summaryobject
picksarray
disclaimerstringexact 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.