wickworks: The Dumb-As-Rocks OHLC Analyzer That Refuses to Tell You What to Buy

Stop telling me when to buy.

That’s the entire ask. Don’t draw arrows on my chart. Don’t fire “BUY SIGNAL CONFIRMED” webhooks. Don’t generate a fake backtested winrate. Don’t sell me a $97/mo Discord. Just tell me where the RSI is right now, where the recent order block sits, where price stands relative to its session VWAP. The interpretation layer is mine. That’s the part I’m not outsourcing.

Every TA SaaS on the internet sells the opposite. They wrap fifteen indicators in a black box, slap “AI-powered” on the marketing page, and charge a monthly subscription for an opaque “signal” that’s right 51% of the time on a good week. The pitch is “we did the hard part for you.” The hard part is the part you should never let someone else do.

wickworks is what the opposite of that looks like.

Bars in, primitives out

docker run --rm -p 8000:8000 psyb0t/wickworks:latest

Two endpoints. GET /health returns { "ok": true, "version": "..." }. POST / takes your OHLC bars and a map of indicators you want, and returns those indicators. Nothing else. No state, no database, no queue, no auth wall, no rate limit, no API key. The container is stateless and idempotent — same bars in, same bytes out, every time. Run ten replicas behind a load balancer and they all agree on the math.

curl -s -X POST http://localhost:8000/ \
  -H 'Content-Type: application/json' \
  -d '{
    "bars": [ {"time":1700000000,"open":1.0832,"high":1.0851,"low":1.0828,"close":1.0844,"tickVolume":1247}, ... ],
    "indicators": {
      "rsi":         true,
      "rsi21":       { "type": "rsi", "length": 21 },
      "macd":        true,
      "orderBlocks": true,
      "fvg":         true
    }
  }'

The keys in your indicators map are the keys you get back. Ask for rsi, get rsi. Ask for rsi21 with "type": "rsi" and a length, get a second RSI under that name. Stack four stochs under different parameters in a single call — four keys, four objects, four separate outputs. The API can’t return data you didn’t ask for, and it can’t return duplicate keys because JSON objects can’t have them. You cannot misuse it.

What’s in the box

The catalog spans the standard TA universe and then some. Every output is camelCase, NaN-safe (warmup positions are null, never a literal NaN that breaks downstream parsers), and serialized through a cleanup layer so no numpy.float64(...) leaks make it out the door.

  • 18 moving averages — SMA, EMA, HMA, WMA, DEMA, TEMA, T3, KAMA, ALMA, linreg, JMA, ZLMA, RMA, FWMA, SWMA, sinwma, TRIMA, VWMA. Plus session-anchored VWAP with daily/weekly/monthly resets and a sessionOffset param so NY (-5h) or EET (-2h) sessions anchor where they should.
  • Momentum oscillators — RSI, MFI, Williams %R, CCI, ROC, MOM, Ultimate Oscillator, Stochastic, StochRSI, MACD, TSI, TRIX, Ehlers Fisher Transform.
  • Trend strength & direction — ADX with +DI/-DI, Aroon, Vortex.
  • Volatility & bands — ATR, NATR, Bollinger Bands, Keltner Channels, Donchian Channels, TTM Squeeze (with explicit on/off/no state flags per bar — the squeeze-release bar is a discrete event you can detect, not a guess).
  • Trailing trend signals — Supertrend, Parabolic SAR, Chandelier Exit, Ichimoku cloud. Pick one — they’re variations on the same idea with different lag/whipsaw tradeoffs.
  • Volume / money flow — OBV, A/D, CMF, A/D Oscillator, Klinger.
  • Smart Money Concepts — Order Blocks, Fair Value Gaps, BOS/CHoCH structure breaks, swing levels, in-house S/R levels, liquidity zones, retracements, sessions, previous-period highs and lows.

The full schema with every parameter and return shape lives in schema.json — JSON Schema Draft 2020-12, suitable for autogenerating typed clients in whatever language you want.

SMC done properly

Smart Money Concepts is half genuinely useful price-action analysis and half YouTube-influencer content. Wickworks does the useful half and skips the dogma.

Order Blocks and Fair Value Gaps come back unmitigated only. The moment price trades back into a zone, the zone is consumed — its liquidity, presumed used. Wickworks filters those out server-side. You get the live zones: the ones price hasn’t yet revisited, sorted ascending by distance to current price, capped at 20 blocks and 15 FVGs. No history dump, no “here’s every order block since the dawn of time” payload bloat. The zones that still matter, ranked by how soon you’d hit them.

BOS vs CHoCH are structural facts, not signals. A Break of Structure (price takes out the last swing in the trend’s direction) is the chart saying “the trend continued.” A Change of Character (price breaks against the prior trend’s structure for the first time) is the chart saying “the trend just got broken for the first time.” Wickworks emits the event with its level and direction. It does not emit “the trend has reversed, buy now.” That’s your read on the event. The chart sets the facts, you set the interpretation.

The S/R-level algorithm is in-house and worth describing. Take swing pivots from a 7-bar swing detector. Keep only pivots that price has tested ≥2 times (a “test” = a high or low within ½·ATR of the level, with bars between the touches). Enforce ≥3·ATR spacing between kept levels so you don’t get three nearly-identical levels stacked on top of each other and counted as three. Return up to three closest above current price (resistance) and three closest below (support). That’s what most traders mean when they draw S/R lines manually — actual tested levels with meaningful separation — and almost no automated indicator does it that way. Most return the raw swing list and call it support.

No signals. Ever.

This is the v0.3.0 stance and it’s load-bearing for the whole project:

  • No divergence detection.
  • No MACD-cross events.
  • No “golden cross” / “death cross” tags.
  • No buy/sell labels.
  • No “this is a fresh signal” flags on event outputs.

Every value in the response is one of: a raw indicator series, a structural fact (an order block formed at this bar; price closed past this swing level), or a pre-baked numerical summary over those (last close, last RSI, current position vs key MAs). Never a judgment. If you want divergences, build them. If you want crossover events, build them. They’re trivial to compute on top of the primitives wickworks returns, they live in your code, and you can iterate on them without redeploying a Python service or paying anyone.

That stance is the entire reason this project exists. Everything else — the catalog size, the response shape, the test suite, the licensing — falls out of it.

“Just give me the current state”

There’s a fast path for clients that don’t want full Series, just the last bar’s snapshot. Six parameterless outputs share a single analysis pass — request all six and they cost the same as requesting one:

"indicators": {
  "price":    true,
  "levels":   true,
  "momentum": true,
  "volume":   true,
  "position": true,
  "slope":    true
}
  • price — last close.
  • levels — EMA21, SMA50/100/200, ATR, VWAP, Donchian upper/lower/mid.
  • momentum — RSI, MFI, MACD line/signal/hist, ADX, stochastic K/D.
  • volume — current-vs-recent volume ratio, OBV, an isSpike boolean that’s true when volume’s more than 2× recent average.
  • position — for each of EMA21/SMA50/100/200/VWAP: "above" or "below". Bias map.
  • slope — same keys, "up" or "down" over the last 10 bars. Combine with position and you have the chart’s regime in two short objects.

Receipts

The trust model for a TA service is “do the numbers match what they’re supposed to match.” Wickworks ships 280+ tests in three categories:

  1. Closed-form math diffs. For every standard indicator — RSI, MACD, ATR, Bollinger, Stochastic, Aroon, CCI, Williams %R, ROC, MOM, OBV, Donchian, VWMA, EMA, SMA — the test suite reimplements the formula from scratch in plain numpy/pandas and diffs the last-bar value against wickworks’ output on real EURUSD H1 ticks. Tolerance rtol=1e-5. If pandas_ta drifts or our wiring rots, the diff screams before the Docker image even builds.
  2. smc_fast parity. The SMC layer ships a numba-accelerated port of smartmoneyconcepts for the hot path. Eight tests prove byte-identical output to the upstream library on the same inputs. The fast path can never silently produce different numbers.
  3. Pipeline contract. Determinism (same bars → same response bytes). Append-stability (causal indicators don’t rewrite their own history when new bars arrive). Warmup-region null counts. Indicator isolation (requesting two together produces the same numbers as requesting them separately). HTTP error paths. Volume-field contract.

And the package set is locked behind uv‘s exclude-newer mechanism: any dependency version published after a fixed date is refused at lock-resolution time. The date is bumped automatically by the package-mutation Make targets — so if you don’t run make pkg-add or make pkg-update, the date doesn’t move, and a freshly-published malicious release that’s still in its detection window can’t sneak into a passive lock refresh. Boring, paranoid, correct.

It tells you when you fucked up

Ask for an sma with length: 200 when you only sent 100 bars and the entire request is rejected up front — not silently returned as a 100-element array of null. The response lists every indicator you under-fed in one shot, so you fix the whole call in one round trip:

{
  "detail": {
    "error": "insufficient_bars",
    "message": "insufficient bars: have 30, but: slowSma (type=sma) needs 200, longRsi (type=rsi) needs 51",
    "available": 30,
    "deficits": [
      { "outputKey": "slowSma", "type": "sma", "required": 200, "available": 30 },
      { "outputKey": "longRsi", "type": "rsi", "required": 51,  "available": 30 }
    ]
  }
}

The required bar count is computed per-indicator from its params, not a global floor. SMC outputs share a MIN_BARS baseline (default 50) because the structural pipeline assumes meaningful history. Total error surface is exactly four codes: 400 (this — structured body), 413 (over MAX_BARS, default 5000), 422 (Pydantic schema failure on the bar payload), 500 (open an issue, this shouldn’t happen).

How it plugs into things

Wickworks is the central TA service in the psyb0t stack. mt5-httpapi embeds it as a sidecar locked to the mt5 container’s network namespace — no published ports, no separate deploy. The mt5 API exposes POST /symbols/:symbol/rates/ta: fetch the candles from MT5, forward them to wickworks under the hood, return the indicator JSON to the client. One call. The client never has to know wickworks exists.

Backtesters do the same. Alert services do the same. Scrapers that need to compute live indicators on streaming OHLC data do the same. One container, one set of math, one set of tests pinning that math. Every consumer is a thin HTTP client that knows how to send bars and parse the response — and the response is the same shape regardless of who’s asking.

Configuration is four env vars, sensible defaults, no required config: LOG_LEVEL=INFO, MAX_BARS=5000, MIN_BARS=50, WORKERS=2. Pull the image, run the container, point your clients at :8000.

Own the math

The pitch reduces to one sentence. Stop renting opinions about your own charts. Compute the primitives yourself, on your own hardware, with math you can audit and tests you can read. Build the interpretation layer in code you control. Iterate on it as fast as you can deploy. Never wait on a Discord channel to tell you when the order block matters.
WTFPL licensed. Do what the fuck you want with it.
github.com/psyb0t/docker-wickworks · hub.docker.com/r/psyb0t/wickworks