How the market-state signals are computed.
Every patternfetch response links here, so here is the honest version: exactly how each field of a brief is derived from raw candles, what each score means, and — just as important — what it does not mean. No black box, no magic, no implied edge.
Binance + YahoostocksETFscrypto1m → 1w
A brief is deterministic: the same candles produce the same output. Nothing here is trained, fitted to future returns, or tuned to "win" — these are transparent, classical techniques applied to recent price history and packaged compactly so an AI agent can reason over them without dumping a raw OHLCV array into its context. Read the limitations section before you act on anything.
01Data source
Inputs are OHLCV candles (open, high, low, close, volume) for one ticker and timeframe. patternfetch covers US stocks, ETFs and crypto in one schema: crypto candles come from Binance spot (realtime), while stocks & ETFs come from Yahoo. Supported timeframes are 1m, 5m, 15m, 30m, 1h, 4h, 1d and 1w. The header.src field records which source produced the series.
Equity data path (stocks & ETFs)
Stock and ETF candles come from Yahoo on an adjusted-close basis — split- and dividend-adjusted, so prices and the patterns computed from them stay continuous across splits and dividend payments. Equity data is delayed / end-of-day rather than realtime, while crypto stays realtime. Everything downstream — codec, patterns, levels, regime, indicators — is computed identically regardless of asset class.
A brief fetches up to ~200 candles for the requested timeframe (the no-signup /v1/demo uses 120). If the series has fewer than 20 candles, the API returns INSUFFICIENT_DATA instead of computing a brief on too little history. Every value below is a point-in-time snapshot over exactly those candles.
02Compact candle codec (codec.rows)
The candle series is encoded for tokens, not for humans. Each candle becomes one row "o,h,l,c,v", and rows are joined by ;. There is no per-row timestamp — the header's t0/t1 (epoch milliseconds) bound the whole series, and the timeframe fixes the spacing, so the index of a row is its position in time.
Prices are written at a single fixed precision (the number of decimal places), with trailing zeros stripped. The precision is chosen from the magnitude of the maximum high in the series:
| Max high in series | Price decimals (precision) |
|---|---|
| ≥ 1000 | 1 |
| ≥ 1 and < 1000 | 2 |
| ≥ 0.01 and < 1 | 5 |
| < 0.01 | 8 |
Volume is written at 0 decimals when ≥ 1, otherwise at 4 decimals. The codec is loss-tolerant by design: it preserves the shape the downstream analysis needs while keeping the payload small. The precision value is returned so a consumer can interpret the digits correctly.
"codec": {
"rows": "60125.4,60480,59890.1,60310.7,1284;60310.7,60590,60180,60420.2,977;…",
"sax": "dcefdcbe",
"precision": 1
}
03SAX shape signature (codec.sax)
The sax string is a coarse fingerprint of the recent shape of the series — a way for an agent to compare or recall shapes cheaply. It is computed as Symbolic Aggregate approXimation over the series of close-to-close returns:
- Take consecutive close-to-close returns.
- z-normalize them (subtract the mean, divide by the standard deviation).
- PAA-collapse the normalized series into at most 8 segments (Piecewise Aggregate Approximation — average within each segment).
- Quantize each segment into the alphabet a–g using the fixed breakpoints [-1.2, -0.5, -0.15, 0.15, 0.5, 1.2] (so values below −1.2 → a, between −1.2 and −0.5 → b, … above 1.2 → g).
The length of the string is min(8, candles − 1). It is a shape descriptor, not a forecast — two identical sax strings mean two stretches looked alike after normalization, nothing about what comes next.
04Patterns (analysis.patterns[])
Patterns are detected geometrically from the candle geometry — there is no model "predicting" them. Each hit is an object:
{ "name": "double_bottom", "confidence": 0.86, "at": 142, "span": 18,
"evidence": { "tf": "4h", "band": "0.75-1.00", "horizon": 10, "n": 1508, "hitRate": 0.475,
"lift": { "baseline": 0.497, "lift": -0.022, "informative": false } } }
- name — one of: bullish_engulfing, bearish_engulfing, hammer, doji, double_top, double_bottom, head_and_shoulders (candlestick and chart patterns).
- confidence — a score in 0..1 for how cleanly the geometry matches the pattern's template (a quality score).
- at — the candle index where the pattern resolves.
- span — how many candles the pattern covers.
Important: confidence is a geometric quality score, not a probability of profit. A 0.86 means the shape is a clean example of that pattern — not that a trade has an 86% chance of working.
04bEvidence — the backtested base rate (analysis.patterns[].evidence)
A clean geometric score is still only a shape match. To stop a confident-looking label from being quoted blindly, each directional pattern also carries an evidence block: how that exact pattern, on that timeframe and confidence band, has actually resolved historically — and, crucially, how often the same market went that way with no pattern present at all. Only the second number makes the first one readable.
"evidence": {
"scope": "major crypto pairs",
"tf": "4h",
"band": "0.75-1.00",
"horizon": 10,
"n": 1508,
"hitRate": 0.475,
"ci95": 0.025,
"dist": { "winRate": 0.475, "median": -0.002, "mean": 0.0, "min": -0.347, "max": 0.495 },
"lift": {
"baseline": 0.496997,
"baselineN": 11990,
"lift": -0.021997,
"ci95": 0.026746,
"ci95Clustered": 0.035172,
"informative": false,
"reading": "indistinguishable-from-baseline"
},
"definition": "realizable gross directional base rate: fraction of non-overlapping historical occurrences whose close-to-close return over the 10 bars AFTER the pattern becomes knowable went its expected direction. Forward window starts at detection (chart patterns: +2 pivot-confirming bars, no lookahead). No stop, fees or slippage. Not a prediction."
}
- hitRate — fraction of past occurrences that moved the pattern's expected direction (bullish patterns up, bearish down) over horizon bars.
- n — number of non-overlapping historical occurrences behind the rate (windows at least horizon bars apart, so one episode is not counted many times). Buckets below 30 samples are omitted, not reported under-powered.
- dist — the full forward-return distribution, never a single headline number; band is the confidence range the rate applies to.
- ci95 — 95% confidence half-width on hitRate itself. It describes the sampling error of that one number; it does not tell you whether the pattern carries information. For that you need lift.
- lift.baseline — the pattern-free rate for the same scope, timeframe and horizon: how often an arbitrary window went that direction with no pattern required. This, not 0.50, is the reference. A drifting market moves every directional rate away from 0.50 on its own — on US equities the daily down-rate is 42.0%, so a bearish pattern scoring 41% has matched the drift, not beaten it.
- lift.lift — hitRate − baseline. The part of the number the pattern is actually responsible for. In the example: 0.475 − 0.497 = −0.022.
- lift.ci95Clustered — 95% half-width on the difference, clustered by calendar day. Use this one. The plain lift.ci95 assumes every occurrence is an independent draw, but a bearish engulfing on AAPL and one on MSFT on the same day are one market move observed twice. On the high-volume daily stock buckets the clustered interval comes out 2.1–2.7× wider — the difference between "this bucket has an edge" and "this bucket is noise".
- lift.reading — above-baseline, below-baseline, or indistinguishable-from-baseline, decided on the clustered interval. Deliberately not a comparison with 50%.
What the measurement actually found. These are realizable gross directional base rates: the forward window starts only at the bar a consumer could actually act on. Chart patterns (double top/bottom, head & shoulders) become knowable two pivot-confirming bars after their low/high prints, so the window starts there — counting from the pivot itself leaks those bars and inflates the rate by ~10 points (a trap most naive backtests fall into; we removed it).
Measured this way and read against the pattern-free baseline, the result is close to uniform: across 105 buckets — 60 on US stocks & ETFs, 45 on major crypto pairs — exactly three clear an unadjusted cluster-robust 95% interval, and all three are crypto. That is fewer than the ~5.3 you would expect from chance alone at that threshold, and none of the three survives a correction for having tested 105 buckets. On US stocks & ETFs the count is 0 of 60. The three are named and tabulated below — a null result that quietly drops its own counter-examples is worth nothing. The geometric confidence barely moves the rate either.
Crypto is the cleanest demonstration. There is no structural drift in the crypto corpus, so its baselines sit at ~50% on every timeframe — and the bullish/bearish split seen on equities simply disappears: bullish engulfing 49.2%, bearish engulfing 49.1% at 4h, both against a ~50% baseline. The split on stocks was never the pattern. It was the drift.
We publish this because the alternative is worse for you: a model handed a bare 41% will narrate a bearish case around it. No stop, no fees, no slippage, not a prediction. Past performance does not guarantee future results.
The three buckets that clear an unadjusted interval
A bucket is flagged when the absolute lift exceeds its cluster-robust 95% interval (lift.informative in the published table). Three of the 105 do, all of them in the crypto corpus:
| Bucket | Timeframe | Confidence band | Lift | Interval (±) | n | z | Reading |
|---|---|---|---|---|---|---|---|
| bullish_engulfing | 1h | 0.75–1.00 | −1.76pp | 1.36 | 8,897 | 2.53 | below baseline |
| double_bottom | 1d | 0.50–0.75 | −12.25pp | 8.91 | 143 | 2.69 | below baseline |
| double_top | 1d | 0.50–0.75 | +9.78pp | 8.42 | 149 | 2.28 | above baseline |
Two of the three sit below their baseline — the pattern resolved its way less often than an arbitrary window did. Only double_top on the daily lands above its baseline, and it is the only bucket of all 105 that does. It rests on n=149 occurrences.
Three things keep that from being an edge:
- The count is below the noise floor. At α=0.05, 105 independent comparisons produce 105 × 0.05 = 5.25 flagged buckets by chance alone even if no pattern carries any information whatsoever. We found 3. The honest summary is not "nothing works" but something stricter: we found fewer standouts than pure noise would have produced.
- None survives multiple-comparison correction. A Bonferroni correction across 105 comparisons moves the two-sided threshold to α=0.05/105 ≈ 0.000476, i.e. |z| > 3.49. The largest z we observe is 2.69. All three fall well short, including the one above baseline.
- The split is lopsided. All three flagged buckets are crypto (3 of 45). On US stocks & ETFs it is 0 of 60 — there the null holds without qualification, on the larger and better-sampled half of the corpus.
We name these rather than round them away because a study that reports only its nulls, and buries the one bucket that went the other way, is committing the error we built this measurement to correct.
The full measurement — all 105 buckets with their baselines, both intervals and sample sizes, the charts, and the limitations in long form — is written up separately: Do chart patterns beat the market's own baseline? A companion measurement counts the tokens the compact brief actually saves against raw candles, with the same insistence on stating limits: What does raw OHLCV cost an LLM in tokens?
04cWhere this measurement is weak
The result above is a null finding, and a null finding is only worth as much as the method behind it. These are the limitations we know about, stated so you can discount our numbers yourself rather than take them on trust.
- Forward windows still overlap across days. Within one ticker, occurrences closer than the horizon are dropped, and the cluster-robust interval treats all same-day occurrences across the ~117 tickers as one shock. But two samples on different days whose 10-bar windows still overlap are not separated by either rule. The clustered interval is therefore a floor, not a ceiling — the true intervals are somewhat wider still, which can only strengthen a null result and would weaken a positive one.
- The ticker universe is survivorship-biased. The stock corpus is a hand-picked set of 117 currently-large US listings and ETFs. Companies that delisted, went bankrupt or fell out of the large-cap set are absent. That inflates the measured upward drift, and therefore the baseline itself. It does not rescue the patterns: both the pattern rate and its baseline are measured on the same biased universe, so the lift is largely insulated — but the absolute baselines (57.8% up on the daily) should be read as an upper bound, not as a market-wide constant.
- Yahoo caps intraday history at roughly 730 days for hourly bars. The 1h and 4h stock buckets therefore cover about two years, while 1d and 1w reach back as far as each listing allows. The intraday buckets sample fewer distinct market regimes, so their baselines are more dependent on which two years happened to be in range.
- Gross directional only. A "hit" is the sign of a close-to-close return. No stop, no target, no fees, no slippage, no position sizing. A pattern with no directional lift could in principle still matter to a strategy that uses it for timing or risk rather than direction — this measurement says nothing about that.
- One horizon, one definition. Everything here is 10 bars ahead, close to close. Patterns are conventionally traded on other horizons and with confirmation rules we do not model. We report what we measured, not that no measurement anywhere could find an effect.
Two of these — overlap and survivorship — make our own numbers look better than they are. We would rather name them than have you find them.
05Support / resistance (analysis.levels)
Levels are built by detecting swing pivots (local highs and lows) in the series and clustering nearby pivots into price levels. Each level is:
{ "price": 59820.4, "strength": 1 }
strength is in 0..1, where 1 is the strongest — i.e. the most-tested level, with the most pivots clustered at it. levels.support holds levels below the current price and levels.resistance holds levels above it. Strength reflects how often price has reacted there in this window, not whether it will hold next time.
06Regime (analysis.regime)
The regime is a compact classification of the current trend state:
{ "trend": "up", "strength": 0.42, "volPct": 2.13 }
- trend — one of up, down, or range.
- strength — a 0..1 score for how pronounced the trend is.
- volPct — volatility as a percentage: the ATR (Average True Range) expressed as a percent of price. A volPct of 2.13 means recent average range is about 2.13% of price.
07Interpreted indicators (analysis.indicators)
Indicators are returned as interpreted state, not a wall of numbers — an LLM reasons better over a label than over a bare float. Each indicator carries its value and a state:
"indicators": {
"rsi": { "v": 58.3, "state": "neutral" },
"ema": { "v": 61240.8, "state": "above_20_50" }
}
- rsi — { "v": <value>, "state": "overbought" | "oversold" | "neutral" }.
- ema — { "v": <value>, "state": "above_20_50" | "below_20_50" | "mixed" | "above_20" | "below_20" }, describing price relative to the 20- and 50-period EMAs.
The v is the raw indicator value if you want it; the state is the digested read your agent can act on directly.
08One-line summary (analysis.nl)
The nl field is a single natural-language line composed from the fields above — specifically the trend, the last-bar % change, the RSI value and state, and the top pattern. It exists so the model never has to do arithmetic on raw numbers (where LLMs hallucinate); the line is already a ready-to-reason summary.
"nl": "BTC/USDT: uptrend (moderate), +1.94% last 4h, RSI 58.3 (neutral), double_bottom (conf 0.86, n=18, hist 51% over 10b)."
It is a faithful restatement of the computed analysis, not an opinion or a call. The hist tail mirrors the top pattern's evidence base rate (section 4b) so the one-line summary can never sound more certain than the data behind it. Note the two n's: in this one-line summary n is the pattern's bar-span (how many candles it covers), while evidence.n is the historical sample size behind the rate.
09Analogs (/v1/analogs)
Analogs do in-series shape matching. patternfetch takes the current window and searches earlier windows of the same series for ones whose shape is similar, using cosine similarity over z-normalized windows. For the similar windows it then looks at what happened over a fixed forward horizon and returns the full outcome distribution:
{ "n": 14, "horizon": 12, "winRate": 0.57, "median": 0.011, "mean": 0.004, "min": -0.083, "max": 0.072 }
- n — how many analogous past windows were found.
- horizon — the fixed number of candles looked ahead.
- winRate — the fraction of analogs whose forward return was positive.
- median, mean, min, max — the distribution of forward returns, expressed as fractions (0.011 = +1.1%).
This is not a prediction and not a strategy backtest. It describes what happened in the past after similar-looking windows — and it includes the losers (that is the whole point of returning min and the full distribution rather than a single rosy number).
10Limitations — read this
We would rather you trust the data because we are candid about its limits:
See it for real, or go deeper:
Related: /docs · /try · /disclaimer