Odel
Wickworks

Wickworks

Local
@psyb0t1PythonWTFPLUpdated Yesterday

Self-hosted MCP server: technical indicators + smart-money-concepts from OHLC bars.

wickworks

CI version license Docker Pulls

The dumb-as-rocks OHLC analyzer. You throw bars at it over HTTP, it throws indicators and SMC objects back. That's the whole product. No database, no queue, no state, no opinions, no "AI-powered signals," no upsell to a $97/mo Discord. Just primitives.

Every snake-oil-flavored TA SaaS out there wants to tell you when to buy. Wickworks tells you the order block is at 1.0832 and the RSI is 71.4. The "what does that mean?" part is where your strategy lives — and it should live in your code, not behind someone else's paywall.

Built on pandas_ta and smartmoneyconcepts, wrapped in a FastAPI server, locked behind 370 tests that diff our output against closed-form references on real EURUSD ticks. If a math bug slips in, the test suite screams before the container builds.

Table of Contents

What's Inside

CategoryPrimitives
TrendSMA/EMA + 15 other moving averages, slope, Donchian channels, Ichimoku
MomentumRSI, MACD, Stochastic, StochRSI, ADX, MFI, CCI, Williams %R, ROC, MOM, TSI, TRIX, UO, Fisher
VolatilityATR, NATR, Bollinger Bands, Keltner Channels, Squeeze
VolumeVWAP (anchored), VWMA, OBV, AD, ADOSC, CMF, KVO
SMCOrder Blocks, Fair Value Gaps, BOS/CHoCH, swing structure, S/R levels, liquidity, retracements, sessions, previous-period H/L
SummariesPosition, slope, momentum, volume regime, recent range — pre-baked projections over the raw series

All JSON field names are camelCase. Output is NaN-safe — NaN becomes null, never a literal NaN token that blows up downstream parsers. NumPy/Pandas scalars and arrays are serialized cleanly (no numpy.float64(...) leaks). Bars in UTC, math in UTC, container runs TZ=UTC — timezone bullshit is your problem, not ours.

Quick Start

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

That's it. The service listens on :8000.

docker compose

services:
  wickworks:
    image: psyb0t/wickworks:latest
    ports: ["8000:8000"]
    environment:
      LOG_LEVEL: INFO
      MAX_BARS: "5000"
      MIN_BARS: "50"

Local development container

make run         # build the dev image and run uvicorn on :8000

API

Three endpoints. That's the whole surface.

GET /health

curl -s http://localhost:8000/health
{ "ok": true, "version": "0.7.0" }

GET /metadata

Returns the versioned catalog of labels, descriptions, units, categories, and interpretation notes for every output path. Consumers can cache it for the reported Wickworks version.

POST / — compute

Send OHLC(V) bars + the indicators you want. Get back only what you asked for — response keys mirror the keys you sent. No "let me also throw in 40 indicators you didn't ask for" energy.

curl -s -X POST http://localhost:8000/ \
  -H 'Content-Type: application/json' \
  -d '{
    "symbol": "EURUSD",
    "timeframe": "H1",
    "bars": [
      { "time": 1700000000, "open": 1.0832, "high": 1.0851, "low": 1.0828, "close": 1.0844, "volume": 1247 },
      ...
    ],
    "indicators": {
      "rsi":         true,
      "rsi21":       { "type": "rsi",   "length": 21 },
      "stochFast":   { "type": "stoch", "k": 5,  "d": 3, "smoothK": 3 },
      "stochSlow":   { "type": "stoch", "k": 21, "d": 7, "smoothK": 5 },
      "macd":        true,
      "orderBlocks": true,
      "fvg":         true
    }
  }'

The indicators object — the whole point

Each entry maps an output name (the key) to a spec:

  • true — run the indicator with default params; key doubles as the type.
  • { ...params } — params object; missing type falls back to the key.
  • { "type": "<name>", ...params } — run a known indicator under a custom output name. This is how you stack multiple instances of the same indicator (e.g. four stochs with different params, three EMAs at different lengths).
"indicators": {
  "rsi":    true,
  "rsi21":  { "type": "rsi",   "length": 21 },
  "stochA": { "type": "stoch", "k": 5,  "d": 3 },
  "stochB": { "type": "stoch", "k": 21, "d": 7 }
}

The response contains rsi, rsi21, stochA, stochB. Nothing else. Duplicate output names are physically impossible by JSON-object construction — you can't shoot yourself in the foot with this API even if you try.

Concepts

The outputs below map to a handful of recurring trading ideas. If you've used any TradingView-style charting tool most of these will be familiar; if not, the short framing here is enough to pick the right output for the job.

Series vs events. A Series output is one value per input bar (warmup positions are null) — these are continuous quantities you can chart. An event output is a sparse array of objects pinpointing things that just happened (a swing, a block, a structure break). Series tell you state; events tell you occurrences.

Primitives only — no signals. Wickworks does not emit interpretive signals (no divergence detection, no MA-cross events, no "buy/sell" tags). Everything returned is either a raw indicator series, a structural fact (an order block was formed at this bar, price closed past this swing), or a pre-baked summary over those — never a judgment about what to do. If you want divergences, MACD-cross events, golden/death crosses, or any other derived signal, build that layer in your own consumer.

The four questions every indicator answers part of:

  1. What's the trend? → Moving averages, ADX, supertrend, ichimoku.
  2. Is momentum behind it? → RSI, MACD, stochastic, MFI.
  3. How much room is there? → ATR, Bollinger Bands, Donchian, Keltner.
  4. Is volume backing it up? → OBV, CMF, A/D, KVO, VWAP.

In-house event constructs you won't find on TradingView:

  • srLevels uses at most the latest 500 bars, a fixed five-bar separation between distinct touches, and at least two tests within half an ATR. It returns up to three nearest levels on each side of price.
  • Order blocks and FVGs expose different state. Order blocks include mitigation state and times so consumers can choose their own freshness rule. FVG output contains only gaps that remain unmitigated.
  • BOS vs CHoCH are classifications produced by the configured Smart Money Concepts swing algorithm. A BOS marks a same-direction structural break. A CHoCH marks an opposite-direction break. Neither label proves continuation or reversal.

Available indicators

Want the formal contract for tooling / validators? See schema.json — full JSON Schema Draft 2020-12. The reference below is the human version: categories + per-indicator blurbs + params tables + return shapes + examples.

Three series shapes are shared across most outputs:

  • Series — one value per bar (number | null), aligned 1:1 with input bars. Warmup positions are null.
  • FlagSeries — one 0/1 integer per bar.
  • DirectionSeries — one -1/+1 integer per bar (1 = bullish/long, -1 = bearish/short).

Indicators below are grouped by what they tell a trader, not by parameter shape. Each subsection starts with a one-paragraph framing of the category, then lists every indicator in it with a short "what it is / when to use it" blurb.


Moving averages — trend bias and dynamic levels

Smoothed price lines. Each flavor trades responsiveness against lag differently — pick by how fast you want the curve to react to new bars. Trader use: define the dominant direction (price above/below the MA = bull/bear bias), identify dynamic support/resistance the market keeps touching, fire crossover signals (fast MA crossing slow MA = trend shift).

All take a single length parameter and return a Series.

typedefaultinputswhat it is
ema21closeExponential MA — recent bars weighted more. Standard trend filter. The default trend MA in most strategies.
sma50closeSimple MA — flat average. Slow, smooth, classic. 50/200-SMA crosses define the "Golden Cross" / "Death Cross".
hma14closeHull MA — low-lag, smooth. Reacts fast without the noise an EMA would give at the same period.
wma14closeWeighted MA — linear weights. Sits between SMA and EMA in lag.
dema10closeDouble-EMA — less lag than EMA via a correction term.
tema10closeTriple-EMA — even less lag, but more whipsaw-prone in chop.
t310closeTillson T3 — smooth like SMA, fast like EMA. Curve-looking output some traders prefer for visual clarity.
kama10closeKaufman Adaptive MA — speeds up in trends, slows down in chop. Self-tuning.
alma10closeArnaud Legoux MA — Gaussian-weighted. Low noise, low lag tradeoff.
linreg14closeLinear-regression MA — best-fit line over the window, evaluated at "now". Statistically grounded smoothing.
jma7closeJurik MA — proprietary smooth, very low lag. Premium-feeling curve.
zlma10closeZero-Lag MA — error-correction on EMA, attempting to remove lag entirely.
rma10closeWilder's smoothing — used inside RSI/ATR. Heavy, slow. Useful when you want indicator-internal smoothing semantics.
fwma10closeFibonacci-weighted MA — weights by Fib sequence.
swma10closeSymmetric-weighted MA — weights peak in the middle of the window.
sinwma14closeSine-weighted MA — sine-curve weights. Very smooth.
trima10closeTriangular MA — double-smoothed SMA. Smoother than SMA, more lag.
vwma10close + volVolume-weighted MA — heavy-volume bars count more. Closer to where actual trading interest was.
"ema":   true,
"ema50": { "type": "ema", "length": 50 }
vwap — session-anchored VWAP

Volume-weighted average price, reset at the configured daily, weekly, or monthly anchor. Unlike a rolling average, VWAP accumulates from that fixed boundary. The sessionOffset shifts the UTC anchor by a fixed duration. It does not apply exchange calendars or daylight-saving changes.

ParamTypeDefaultDescription
anchor"D" | "W" | "M""D"Session reset cadence
sessionOffsetstring | number"0s"Offset session start from UTC midnight. Go-style ("-5h", "1h30m") or seconds.

Returns: Series.


Momentum oscillators — speed and exhaustion

Measure the rate of price change, not the level itself. Most are bounded (0–100 or centered around zero), so readings are directly comparable across instruments and timeframes. Trader use: spot overbought/oversold extremes (mean-reversion edges), watch for divergence vs price (momentum fading while price extends = reversal hint), trade zero-line / midline crosses as momentum-shift triggers.

Length-based single-line oscillators

Same spec: one length parameter, returns one Series.

typedefaultinputsscalewhat it tells you
rsi14close0–100Relative Strength Index. Classic momentum oscillator. >70 overbought, <30 oversold. Bounded, well-studied, the canonical input for momentum-vs-price analysis downstream.
mfi14h/l/c + vol0–100Money Flow Index — RSI weighted by volume. Stricter signal: needs both price and volume agreeing.
willr14h/l/c-100..0Williams %R. Inverted stochastic. -20 ≈ overbought, -80 ≈ oversold. Quick to flip.
cci14(h+l+c)/3unboundedCommodity Channel Index. Measures deviation from a moving average in normalized units. ±100 are conventional thresholds. (In-house implementation — pandas_ta has a known bug.)
roc10close%Rate of Change. Percent move over N bars. Most direct momentum number — no smoothing, no normalization.
mom10closeprice unitsAbsolute momentum: close - close.shift(length). Raw price-unit version of ROC.

cci accepts an extra c parameter (number > 0, default 0.015) — the constant scaling factor in the classic Lambert formula.

uo — Ultimate Oscillator

Williams' combo of three timeframes (short/medium/long) blended into one 0–100 line. Designed specifically to reduce the false signals single-period oscillators give in ranging markets. Watch for divergences and 30/70 extremes — same as RSI but with built-in multi-period confirmation.

ParamTypeDefaultDescription
fastinteger ≥ 17Short period
mediuminteger ≥ 114Medium period
slowinteger ≥ 128Long period

Returns: Series (0–100).

stoch — Stochastic oscillator

"Where is the close within the recent high-to-low range?" Returns %K (raw position) and %D (smoothed %K). Classic signals: %K crossing %D is the trigger; both lines above 80 = overbought zone, below 20 = oversold zone. Like RSI but more reactive — fires more often, false-positives more often too.

ParamTypeDefaultDescription
kinteger ≥ 114Lookback for raw %K
dinteger ≥ 13%D smoothing
smoothKinteger ≥ 13%K smoothing

Returns: { k, d } — each a Series (0–100).

stochrsi — Stochastic of RSI

Stochastic formula applied to RSI values instead of price. Doubly sensitive — fires far more frequently than vanilla stoch and is especially good at picking turning points inside a ranging move. Pair with a trend filter; on its own it overtrades.

ParamTypeDefaultDescription
lengthinteger ≥ 114Stoch lookback over RSI
rsiLengthinteger ≥ 114RSI period (input to Stoch)
kinteger ≥ 13%K smoothing
dinteger ≥ 13%D smoothing

Returns: { k, d } — each a Series (0–1, not 0–100).

macd — Moving Average Convergence Divergence

Difference between a fast and slow EMA, plus a signal-line smoothing of that difference. Three lenses: the macd line (raw momentum), the signal line (smoothed), the hist (macd − signal — what most traders actually watch). Hist crossing zero = momentum direction change; hist diverging from price = momentum exhaustion.

ParamTypeDefaultDescription
fastinteger ≥ 112Fast EMA
slowinteger ≥ 126Slow EMA
signalinteger ≥ 19Signal-line EMA over (fast − slow)

Returns: { macd, signal, hist } — each a Series. hist = macd - signal.

tsi / trix / fisher — momentum with signal line

All return the same shape: { <name>: Series, signal: Series }. Watch zero-line crosses and value-vs-signal crosses, same as MACD.

tsi — True Strength Index. Double-smoothed price momentum (close-based). Smoother than MACD, slower to flip — fewer false signals, more lag.

ParamTypeDefault
fastinteger ≥ 113
slowinteger ≥ 125
signalinteger ≥ 113

trix — Triple-smoothed exponential ROC. By design it filters out cycles shorter than its length, so it's a longer-term momentum read — useful for higher-timeframe trend confirmation, not scalping.

ParamTypeDefaultDescription
lengthinteger ≥ 130EMA chain length for triple smoothing
signalinteger ≥ 19Signal-line EMA over trix

fisher — Ehlers Fisher Transform. Reshapes price into a Gaussian-like distribution so extremes are sharper and turning points are easier to spot than in RSI.

ParamTypeDefault
lengthinteger ≥ 19
signalinteger ≥ 11

Trend strength & cross-direction

These don't tell you the price level — they tell you how trendy the market is right now, or which side is in control. Pair them with a price-based indicator: trend-strength tells you whether to trust trend signals at all.

adx — ADX + DMI

Average Directional Index measures trend strength only, not direction. adx rises when one side is winning decisively (regardless of which side). The +DI and -DI lines are the directional pressure components — +DI > -DI = bulls in control, and vice versa. Rule of thumb: adx > 25 = market is trendable, follow signals; adx < 20 = chop, avoid trend strategies and prefer mean-reversion.

ParamTypeDefault
lengthinteger ≥ 114

Returns: { adx, diPlus, diMinus } — each a Series.

aroon

Race between "how many bars since the highest high?" and "how many bars since the lowest low?", normalized 0–100. up near 100 = recent action keeps making new highs (strong uptrend); down near 100 = recent lows (strong downtrend). oscillator = up - down is the net directional read on the same -100..+100 scale.

ParamTypeDefault
lengthinteger ≥ 114

Returns: { up, down, oscillator }up/down are 0–100; oscillator ranges -100..+100.

vortex

Two lines measuring positive (plus) vs negative (minus) true-range movement. Pure trend-flip detector: plus crossing above minus = bullish shift; the inverse = bearish shift. No overbought/oversold concept here.

ParamTypeDefault
lengthinteger ≥ 114

Returns: { plus, minus } — each a Series.


Volatility

Measure the spread of price action, not its direction. These don't generate buy/sell signals on their own — they're inputs to stop placement (don't set a stop tighter than 1–2 ATR), position sizing (size inversely to volatility so each trade risks the same dollar amount), and regime detection (rising volatility = breakout regime; collapsing volatility = consolidation, watch for squeeze).

typedefaultinputsscalewhat it tells you
atr14h/l/cprice unitsAverage True Range — average bar range over N bars, in raw price units. The universal stop-distance unit.
natr14h/l/c% of priceNormalized ATR — ATR as a percentage of close. Same information, comparable across instruments at different price levels.

Volume / money flow

Volume-derived lines compare price movement with the caller-selected activity series. They can show whether activity expanded or contracted with a move, but they do not identify participants or prove that a breakout is real.

obv and ad — parameterless cumulative lines

No params. Returns: Series.

  • obv — On-Balance Volume. Adds volume on up bars, subtracts on down bars. Cumulative running total. When OBV diverges from price (price up, OBV flat or down) = warning sign that the move lacks volume backing.
  • ad — Accumulation/Distribution. Weighted by where close lands within the bar's range (close near high = mostly buying; close near low = mostly selling). Cumulative. More precise than OBV when bars have long ranges.
cmf — Chaikin Money Flow

The A/D formula normalized to a rolling window instead of accumulating forever. Returns -1..+1: positive = net accumulation pressure over the window, negative = distribution. Use the zero line as a regime filter — only take longs when CMF is positive.

ParamTypeDefault
lengthinteger ≥ 120

Returns: Series.

adosc — Chaikin A/D Oscillator

MACD-style oscillator built over the A/D line — fast EMA minus slow EMA of A/D. Detects shifts in accumulation momentum (acceleration), not just direction. Zero-line crosses signal regime change in volume pressure.

ParamTypeDefaultDescription
fastinteger ≥ 13Fast EMA over the A/D line
slowinteger ≥ 110Slow EMA over the A/D line

Returns: Series.

kvo — Klinger Volume Oscillator

Volume-force indicator with signal line. Designed to spot long-term reversals while staying sensitive to short-term swings — the dual-period structure (fast/slow) makes it useful both as a primary signal and as a confirmation overlay. Requires volume bars.

ParamTypeDefault
fastinteger ≥ 134
slowinteger ≥ 155
signalinteger ≥ 113

Returns: { kvo, signal } — each a Series.


Bands & channels — dynamic price envelopes

Lines wrapping price action. Three trader uses: mean-reversion edges (touch upper band = stretched up, fade candidate; touch lower = stretched down), breakout triggers (close outside the band = volatility regime change), and squeeze detection (bands narrowing = compression preceding expansion). The three flavors below use different math (std-dev vs ATR vs raw range) but serve the same role.

bbands — Bollinger Bands

SMA ± N standard deviations. The width self-adapts to recent volatility. Classic 2σ touch in theory contains ~95% of bars; in practice price walks the band in strong trends, so don't blindly fade band-touches in a trend.

ParamTypeDefaultDescription
lengthinteger ≥ 120SMA window
stdnumber > 02.0Std-dev band width

Returns: { upper, middle, lower } — each a Series.

kc — Keltner Channels

Like Bollinger but uses ATR instead of standard deviation for width. Smoother — doesn't react as sharply to a single outlier bar. Often paired with bbands as a squeeze detector (see squeeze below).

ParamTypeDefaultDescription
lengthinteger ≥ 120EMA / ATR window
scalarnumber > 02.0ATR multiplier

Returns: { upper, middle, lower } — each a Series.

donchian — Donchian Channels

Rolling max(high) and min(low) over N bars. The original "Turtle Trader" channel — breaking above the upper = new N-bar high = trend-long signal; breaking below the lower = new N-bar low = trend-short signal. Brutally simple, surprisingly effective on trending instruments.

ParamTypeDefault
lengthinteger ≥ 120

Returns: { upper, middle, lower }upper/lower are rolling max/min; middle is their midpoint.


Trailing trend signals

Single-line trend filters that flip direction with the trend. Two roles: trend filter (only take longs when bullish, only shorts when bearish) and trailing stop (the line value is where you'd exit if the trend reverses). Use one — they're all variations on the same idea, with different lag/whipsaw tradeoffs.

supertrend

ATR-based trailing band. When price is above, the band sits below acting as a trailing-stop support line; when price closes through it, the band jumps to the opposite side and flips direction. The default 7×3.0 ATR is the canonical "TradingView Supertrend" setting.

ParamTypeDefaultDescription
lengthinteger ≥ 17ATR period
multipliernumber > 03.0ATR multiplier

Returns: { value, direction, long, short }value is the trailing band (Series); direction is DirectionSeries; long/short carry the band value only on that direction's leg, null otherwise (so you can plot two distinct-colored series).

psar — Parabolic SAR

Wilder's "stop and reverse" — dots that accelerate toward price during a trend, flipping to the other side when hit. Tightest of the trailing-stop family: dots get close to price fast. Brilliant in clean trends; disastrous in chop, where it whipsaws constantly.

ParamTypeDefaultDescription
afnumber > 00.02Acceleration step per bar
maxnumber > 00.2Maximum acceleration factor

Returns: { long, short, af, reversal }long/short are the SAR value on that leg (Series, null on the other); af is the current acceleration value (Series); reversal is a FlagSeries (1 = trend flipped this bar).

chandelierExit

ATR-based trailing stop pinned to the highest high (long leg) or lowest low (short leg) over a lookback window. Trails further from price than supertrend — wider stops, fewer flips. Good for swing trading where you want to give the trend room to breathe.

ParamTypeDefaultDescription
lengthinteger ≥ 122High/low lookback
atrLengthinteger ≥ 122ATR period
multipliernumber > 02.0ATR multiplier

Returns: { long, short, direction }long/short are exit levels on that leg (Series, null otherwise); direction is DirectionSeries.

ichimoku

Five-line Japanese trend system. The "cloud" (between spanA and spanB, projected kijun bars into the future) is the headline read: price above the cloud = bullish regime; price inside the cloud = neutral/chop, no high-conviction trades; price below the cloud = bearish regime. tenkan (fast) and kijun (slow) are midpoint lines used for crossover triggers; chikou is the close shifted back, used to confirm signals against historical price.

ParamTypeDefaultDescription
tenkaninteger ≥ 19Conversion line — fast midpoint of high/low
kijuninteger ≥ 126Base line — slow midpoint
senkouinteger ≥ 152Leading span B period

Returns: { spanA, spanB, tenkan, kijun, chikou } — all Series in price units. spanA/spanB are forward-projected by kijun bars (the cloud lives in the future); chikou is close shifted back by kijun bars.


Compression / regime

squeeze — TTM Squeeze

Detects when Bollinger Bands sit entirely inside Keltner Channels — i.e., realized volatility (the BB width) has dropped below average volatility (the KC width). The market is compressing, coiling. Squeeze releases historically precede sharp directional moves: when off fires (squeeze just released this bar), trade the breakout direction indicated by value.

The flag fields form a per-bar state machine:

  • on — squeeze is active this bar (BB inside KC). Market is coiled.
  • off — squeeze just released this bar (BB exited KC). Trigger bar.
  • no — no squeeze (default state).
ParamTypeDefault
bbLengthinteger ≥ 120
bbStdnumber > 02.0
kcLengthinteger ≥ 120
kcScalarnumber > 01.5

Returns: { value, on, off, no }value is signed momentum (Series, positive = bullish momentum during/after squeeze, negative = bearish); on/off/no are FlagSeries.

SMC primitives

Smart Money Concepts is a price-action framework that labels zones, levels, and structural shifts from OHLCV patterns. Wickworks reports what the configured algorithms detected. It does not observe institutional orders or prove that a level will cause a reaction.

The core SMC ideas you'll see in the outputs below:

  • Order block is an algorithmically detected zone around the last opposite-direction candle before a qualifying move. Some SMC methods watch a later retest, but the calculation does not observe orders or predict a reaction.
  • Fair Value Gap (FVG) is a three-bar pattern where the first and third wicks do not overlap. The output records the detected gap and its mitigation state. It does not predict a revisit.
  • Mitigation describes a later price return into a zone. Order blocks retain mitigation fields. FVG output contains only gaps that remain unmitigated.
  • Swing high/low — local pivots in price structure. SMC trend identification is built on the sequence of swings (HH/HL = uptrend, LH/LL = downtrend).
  • BOS (Break of Structure) marks a same-direction break of a swing level under the configured algorithm.
  • CHoCH (Change of Character) marks an opposite-direction break under the configured algorithm. It is not proof of a reversal.
  • Liquidity — clusters of presumed stop-loss orders that sit above equal highs / below equal lows. Price tends to "hunt liquidity" before reversing.

Shared object shapes

Price zone (used by orderBlocks and fvg):

{
  "type": "bullish|bearish",
  "top": 1.0892,
  "bottom": 1.0871,
  "candleIdx": 312,
  "time": 1700123400,
  "originAt": 1700123400,
  "confirmedAt": 1700127000,
  "observedAsOf": 1700200000,
  "distancePct": 0.082
}
FieldTypeDescription
type"bullish" or "bearish"Zone direction. Bullish means support; bearish means resistance.
topnumberUpper price boundary of the zone.
bottomnumberLower price boundary of the zone.
candleIdxinteger ≥ 0Zero-based index of the originating bar in the submitted bars.
timeintegerOrigin bar time in UTC seconds.
originAtintegerOrigin bar time in UTC seconds.
confirmedAtintegerFirst bar time when the zone was knowable.
observedAsOfintegerLast submitted bar time used to build the snapshot.
distancePctnumberAbsolute distance from the reference edge to current price, divided by price.

orderBlocks also returns mitigatedWickAt, mitigatedCloseAt, mitigatedWick, mitigatedClose, touchCount, and touchEvents. The FVG output includes only unmitigated gaps, so it does not return those fields.

BosChochEvent, SwingLevel, SrLevel, RecentRange:

| Output | Object shape | | -------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------- | ------------------------ | ---------------------- | | bosChoch events | { event: "BOS" | "CHoCH", direction: "bullish" | "bearish", level: number | null, time: integer } | | swingLevels events | { type: "high" | "low", level: number, time: integer } | | srLevels events | { level: number, type: "support" | "resistance", distancePct: number, touches: integer ≥ 2 } | | recentRange | { high: number, low: number, periodHigh: number, periodLow: number } |

SmcEvent (used by liquidity, previousHighLow, sessions, retracements): sparse object with idx + time always present; remaining fields forwarded as-is from the underlying smartmoneyconcepts library (null fields stripped to keep payloads small).

Outputs

orderBlocks

Order blocks across the submitted history, including wick and close mitigation state, mitigation times, and touch events. Consumers decide whether to show only fresh zones.

  • Params: none.
  • Returns: array of OrderBlock, max 40, sorted ascending by |distancePct| (closest to current price first).
fvg (alias fvgs)

Unmitigated Fair Value Gaps. They use the shared price-zone shape. Some SMC methods watch for later gap revisits, but Wickworks does not predict a fill or reversal.

  • Params: none.
  • Returns: array of OrderBlock-shaped objects, max 15, sorted ascending by distance.
bosChoch

Recent structural events classified as BOS or CHoCH by the configured swing algorithm. Consumers can combine the event sequence with their own trend definition and confirmation rules.

  • Params: none.
  • Returns: array of BosChochEvent, max 10, scanned across the trailing 50 bars.
swingLevels

Recent confirmed swing highs and lows from the structural pass. The raw inputs SMC uses to define trend. Useful for plotting structure or for building your own break-detection logic on top of ours.

  • Params: none.
  • Returns: array of SwingLevel, max 10, scanned across the trailing 50 bars.
srLevels

In-house support / resistance levels. Construction uses at most the latest 500 bars, takes pivots from the 7-bar swing detector (sw7), requires at least two touches within half an ATR with a minimum five-bar separation, enforces three-ATR spacing between returned levels, and returns up to three levels on each side of price ranked by proximity.

  • Params: none.
  • Returns: array of SrLevel, up to 3 support + 3 resistance.
recentRange

Compact summary of the chart's range: last 20 bars vs the full submitted history. Quick context for "is current price near recent highs / lows?"

  • Params: none.
  • Returns: RecentRange object ({ high, low, periodHigh, periodLow }).
liquidity

Equal-high and equal-low clusters detected from swing levels within the configured range tolerance. Some SMC methods call these liquidity areas, but Wickworks does not observe orders or predict a sweep or reversal.

ParamTypeDefaultDescription
swingLengthinteger ≥ 110Swing-detection window
rangePercentnumber > 00.01Max % distance for equal-high / equal-low grouping

Returns: array of SmcEvent.

previousHighLow

High and low markers from the immediately completed fixed UTC aggregation period. This is not an exchange-session calculation.

ParamTypeDefaultDescription
timeFramestring"1D"Despite its legacy name, this selects the previous-range aggregation bucket, not the chart timeframe. It uses fixed UTC-aligned periods, not exchange sessions. Canonical values: 15m, 30m, 1H, 4H, 1D, 1W, 1M. VoidAlpha's existing 15min, 30min, 1h, 4h, 1d, 1w, and 1ME spellings are also accepted. Weeks run Monday through Sunday.

Returns: array of SmcEvent.

sessions

Marks bars inside a fixed UTC clock window. Named windows do not use exchange calendars and do not adjust for holidays or daylight saving time.

ParamTypeDefaultDescription
sessionstring"London"Named UTC session window. Use "Custom" for another window.
startTime"HH:MM" | nullnullRequired UTC start time for "Custom"; rejected for named sessions.
endTime"HH:MM" | nullnullRequired UTC end time for "Custom"; rejected for named sessions.

Returns: array of SmcEvent.

retracements

Fibonacci retracement events relative to the most recent swing. Trader use: standard Fib levels (38.2 / 50 / 61.8) on the active leg, computed automatically without you picking the swing endpoints.

ParamTypeDefaultDescription
swingLengthinteger ≥ 110Swing-detection window

Returns: array of SmcEvent.

Analysis summaries

These outputs are last-bar snapshots computed in a single shared analysis pass. The pass runs once per request and stores its results; each output below just reads its slice from that cached result. Requesting all six costs the same as requesting one — these are essentially free if you already need any of them.

Use them when you want a structured "current state" view of the chart in one shot, without subscribing to full Series outputs and reading only the last value yourself.

All are parameterless.

OutputReturnsTrader use
pricenumber — last bar's close (plain scalar, not an object)Reference price for distance/level calculations.
levels{ ema21, sma50, sma100, sma200, atr, vwap, donchianUpper, donchianLower, donchianMid } — each number | nullSnapshot of standard MAs + ATR + VWAP + Donchian. The "where are the levels right now" view.
momentum{ rsi, mfi, macdHist, macdLine, macdSignal, adx, stochK, stochD } — each number | nullOne-glance momentum scorecard across the popular oscillators.
volume{ volRatio: number|null, obv: number|null, isSpike: boolean }isSpike is true when volRatio > 2.0Quick "is the current bar a volume spike?" check. volRatio is current volume / recent average.
positionobject — keys from {ema21, sma50, sma100, sma200, vwap}, each "above" | "below". Only keys whose MA is computable appear.Bias map: is price above or below each major reference line right now?
slopeobject — same keys as position, each "up" | "down". Direction over the last 10 bars.Are the lines themselves rising or falling? Combines with position for full regime read.

Request fields

FieldTypeDefaultRequiredDescription
barsarray of BaryesOHLC(V) bars in chronological order. len(bars) <= MAX_BARS (default 5000).
indicatorsobjectyesMap of outputKey → spec. ≥ 1 entry.
symbolstring""noCaller-supplied instrument label, echoed unchanged.
timeframestring""noOpaque chart bar interval label, such as H4. Wickworks echoes it and attaches it to summaries, but does not parse it or use it to bucket bars. Calculations use the submitted values and timestamps.

Bar shape:

FieldTypeDefaultRequiredDescription
timeintegeryesUTC unix seconds
open, high, low, closenumberyesOHLC prices
volumenumber ≥ 0requiredyesCaller-selected activity or volume series. Wickworks does not interpret vendor-specific volume types.

Spec value for each indicators entry:

  • true — run the indicator named by the output key with default params.
  • { ...params } — params object. Missing type falls back to the output key. Include "type" to run a known indicator under a custom output name.

Response

{
  "symbol": "EURUSD",
  "timeframe": "H1",
  "candles": 500,

  "rsi": [null, null, 71.4],
  "rsi21": [null, null, 64.2],
  "stochFast": { "k": [78.4], "d": [72.1] },
  "macd": { "macd": [0.00124], "signal": [0.00098], "hist": [0.00026] },
  "orderBlocks": [
    {
      "type": "bullish",
      "top": 1.0892,
      "bottom": 1.0871,
      "candleIdx": 312,
      "time": 1700123400,
      "distancePct": 0.082
    }
  ],
  "fvg": [
    {
      "type": "bearish",
      "top": 1.0945,
      "bottom": 1.0938,
      "candleIdx": 401,
      "time": 1700152800,
      "distancePct": 0.063
    }
  ],
  "bosChoch": [
    {
      "event": "BOS",
      "direction": "bullish",
      "level": 1.0918,
      "time": 1700125200
    }
  ]
}
FieldTypeDescription
symbolstringEcho of the request field
timeframestringEcho of the request field
candlesinteger ≥ 0Number of bars processed
(arbitrary)variesOne entry per output key from indicators. Value shape is the return of the requested indicator.

Only the keys you requested. Plus symbol, timeframe, candles. Warmup positions in Series outputs are null, never NaN.

Errors

StatusReason
400empty bars, empty indicators, unknown indicator type, malformed indicator spec, or insufficient bars for one or more requested indicators (structured body — see below)
413len(bars) > MAX_BARS
422bar payload fails Pydantic schema validation (missing required field, wrong type, etc.)
500internal computation error (should never happen — open an issue with the request body)

When you ask for an indicator that needs more bars than you sent (e.g. sma length=200 with 100 bars), the request is rejected up front — no silent all-null series. The response lists every under-fed indicator at once so you can 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 per-indicator requirement is derived from its params (length, slow, signal, etc.) — not a global floor. SMC-backed outputs (orderBlocks, fvgs, bosChoch, summaries, …) share a baseline floor of MIN_BARS (default 50) because the analysis pipeline assumes meaningful history.

MCP

Everything POST / does is also available over the Model Context Protocol — wickworks mounts a streamable-HTTP MCP server at /mcp in the same process, so an agent can drive it over JSON-RPC without the REST client. It's stateless (stateless_http=True — no initialize handshake, JSON responses rather than SSE) and the tools mirror the REST surface one-for-one:

ToolArgsMirrors
healthGET /health
list_indicatorsthe registered indicator types (the keys compute accepts)
metadataGET /metadata
computebars, indicators, timeframe?, symbol?POST / (same envelope back)
# Native remote MCP (Claude Code, Cursor, … — no bridge needed)
claude mcp add --transport http wickworks http://localhost:8000/mcp

# Raw JSON-RPC — stateless, call tools/call directly
curl -s http://localhost:8000/mcp/ \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
       "params":{"name":"compute","arguments":{
         "bars":[/* ...OHLC... */],"indicators":{"rsi":true}}}}'

The MCP transport's DNS-rebinding Host check is disabled so the endpoint works behind any hostname / reverse proxy — put access control at your proxy, not the app. For MCP clients that only speak local stdio servers, the @psyb0t/wickworks OpenClaw plugin is a thin stdio↔HTTP bridge to /mcp/.

Agent integrations

The skill works in any agent that reads .agents/skills/, and installs natively in the clients below.

Claude Code

claude plugin marketplace add psyb0t/agents
claude plugin install wickworks@psyb0t

Claude Code prompts for the wickworks URL and, if a reverse proxy in front of wickworks requires auth, the bearer token — the token is stored in your OS keychain.

Codex

codex plugin marketplace add psyb0t/agents
codex plugin add wickworks@psyb0t

Installed via the marketplace, the skill invokes as $wickworks:wickworks. Codex also picks the skill up automatically, no install needed, in any repo containing .agents/skills/ — there it invokes as plain $wickworks.

OpenClaw

The skill is published to ClawHub on every release:

openclaw skills install @psyb0t/wickworks

For MCP clients that speak local stdio, the @psyb0t/wickworks plugin bridges to wickworks' /mcp endpoint:

openclaw plugins install clawhub:@psyb0t/wickworks

Then set WICKWORKS_URL (and WICKWORKS_TOKEN if a reverse proxy in front of wickworks requires one).

Configuration

All env-driven. Sensible defaults. Nothing to tune for a first run.

VariableDefaultDescription
LOG_LEVELINFOStandard Python logging level.
MAX_BARS5000Reject requests with more bars than this (HTTP 413).
MIN_BARS50Baseline floor for SMC-backed outputs (orderBlocks, fvgs, summaries, …) and the fallback requirement for any indicator not explicitly listed in the per-indicator requirement table. Series indicators (sma, rsi, macd, …) compute their own min from params.
WORKERS2uvicorn worker count.

Architecture

┌─────────────┐    ┌──────────────────────┐    ┌─────────────────┐
│  Your app   │───▶│  POST /              │───▶│  primitives     │
│  (any lang) │    │  bars in, JSON out   │    │  (camelCase)    │
└─────────────┘    └──────────────────────┘    └─────────────────┘
                              │
                              ▼
                   ┌──────────────────────┐
                   │  pandas_ta + SMC     │
                   │  swing structure     │
                   │  S/R · summaries     │
                   └──────────────────────┘

Stateless. No DB. No queues. No external calls. Bars in → JSON out. Horizontally scale by adding replicas. Two replicas hit the same input deterministically — same bars, same bytes, every time. Test suite pins it.

Development

make help          # list all targets
make run           # production-style uvicorn in the dev container
make test          # full suite (unit + docker integration)
make test-unit     # in-process only — fast feedback loop
make test-docker   # docker-in-docker integration tests
make lint          # flake8 + mypy
make format        # isort + black
make check         # lint + tests

Package management — supply-chain defense

Wickworks uses uv's exclude-newer to refuse any package version published after a fixed date. The date is bumped to today automatically by the package-mutation make targets — so you can't accidentally pull in a freshly-published malicious release that's still in its detection window.

make pkg-add PKG=foo==1.2.3   # bump exclude-newer, then uv add
make pkg-remove PKG=foo       # bump exclude-newer, then uv remove
make pkg-update PKG=foo       # bump exclude-newer, then uv lock --upgrade-package
make pkg-lock                 # refresh the lock using the current exclude-newer cutoff
make pkg-upgrade              # bump exclude-newer + lock --upgrade everything

Never hand-edit [tool.uv].exclude-newer unless you know what you're doing. The bump-on-mutation pattern is the whole point.

Optional ta extras

pandas-ta and smartmoneyconcepts pin conflicting transitive dependencies. The Dockerfiles install them without dependency resolution. They are deliberately absent from [project.dependencies].

Testing philosophy

The test suite is the receipts. 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, MACD…), the suite implements the formula from scratch in plain numpy/pandas and diffs the last-bar value against wickworks' output on real EURUSD H1 data. Tolerance: rtol=1e-5. If pandas_ta or our wiring drifts, the diff catches it before the container ships.
  2. smc_fast parity — wickworks ships a numba-accelerated port of smartmoneyconcepts. Eight parity tests prove the fast path produces byte-identical results to the upstream library on the same inputs.
  3. Pipeline contract tests — determinism (same bars → same bytes), append-stability (causal indicators don't change historical values when new bars arrive), warmup-region None counts, indicator isolation (requesting two together == requesting separately), HTTP error paths, volume field contract.

Run make test-unit for the fast loop, currently 370 tests. Run make test for the full 377-test suite, including seven docker-in-docker integration tests.

License

WTFPL — see LICENSE. Do what the fuck you want.