Signal Reference
Webhook payloads, component params, and anchorPrice flow for Megadrive / test-bot.
Webhook Payload
POST to /api/webhook with Content-Type: application/json.
{
"regime": "CALM", // optional — updates stored regime
"anchorPrice": 67000, // optional — persisted to SQLite
"signal": "grid,fast_mm", // single or comma-separated
"exchange": "deribit",
"symbol": "BTC-PERPETUAL",
"name": "test-bot",
"params": {} // optional — overrides forwarded to component
}
Regime-only update (persists anchor, no component started):
{
"regime": "CALM",
"anchorPrice": 67000,
"exchange": "deribit",
"symbol": "BTC-PERPETUAL",
"name": "test-bot"
}
Signal Routing
| signal | dispatches | notes |
|---|---|---|
start_bot | all regime-gated components | Normal operating mode — respects activeInRegimes per component |
grid | Grid Maker | Cancel existing → buy ladder → sell ladder from position → flip-flop |
main_mm | Main Market Maker | Wide spread, larger allocation |
fast_mm | Fast Market Maker | Tight spread, active book |
slow_mm | Slow Market Maker | Wide spread, patient fills |
flow_mm | Flow Maker | Layered MM with price following and dip accumulation — runs alongside or independently of other MMs |
dip_buyer | Dip Buyer | Laddered trailing limit buys below anchor |
min_order | Min Order | Single permanent anchor bid |
hedge | Hedge (options) | Buys put/call protection |
tp / sell_ladder | Sell Ladder | Ratcheting partial TP ladder — price-triggered trailing sells |
threshold_tp | Threshold TP | Market sell X% of position when position > threshold |
ladder | Scaled Order | One-shot scaled entry — params forwarded directly |
min_size | Limit Order | Single limit order — params forwarded |
options_long_call | Options Open (call) | |
options_long_put | Options Open (put) | |
cancel | Cancel targeted algo loop(s) | Stops a specific bot by tag or side — see Cancel Signal section. Exchange orders pulled by the loop itself within one poll cycle. |
cancel_all | Cancel everything | Stops all algo loops on all exchanges and cancels all open exchange orders. Equivalent to the dashboard kill button. |
Multi-Signal
The signal field accepts a comma-separated list. Each token is dispatched sequentially in order.
{
"regime": "CALM",
"signal": "grid,fast_mm,tp",
"exchange": "deribit",
"symbol": "BTC-PERPETUAL",
"name": "test-bot"
}
Cancel Signal
Stops a targeted set of algo loops without touching anything else. The loop detects the cancellation flag within one polling cycle (typically a few seconds) and pulls its own exchange orders before exiting.
{
"signal": "cancel",
"exchange": "deribit",
"symbol": "BTC-PERPETUAL",
"params": { "which": "mm_fast" }
}
which values
| which | Effect |
|---|---|
mm_main | Stops the main MM loop (tag = main) |
mm_fast | Stops the fast MM loop (tag = fast) |
mm_slow | Stops the slow MM loop (tag = slow) |
grid | Stops the grid loop |
dip_buyer | Stops the dip buyer loop |
hedge | Stops the hedge loop |
buy | Stops all algo loops currently on the buy side |
sell | Stops all algo loops currently on the sell side |
all | Stops all loops on this exchange (scoped to the exchange in the payload) |
Cancel + restart in one payload
Because signal executes left-to-right, you can cancel and immediately re-launch in a single webhook:
{
"signal": "cancel,fast_mm",
"name": "test-bot",
"exchange": "deribit",
"symbol": "BTC-PERPETUAL",
"params": { "which": "mm_fast" }
}
Execution order: cancel flags mm_fast as cancelled → fast_mm launches a fresh instance immediately. which is only read by cancel; the bot signals ignore it.
Cancel + restart with different params
{
"signal": "cancel,main_mm",
"name": "test-bot",
"exchange": "deribit",
"symbol": "BTC-PERPETUAL",
"params": {
"which": "mm_main",
"spread": "0.08%",
"depth": "1.5%"
}
}
Cancelling two different bots
params.which is shared across all signals in the payload, so you can't target two different tags in one message. Use which=all then restart what you want, or send two separate webhooks:
{
"signal": "cancel,main_mm,fast_mm",
"name": "test-bot",
"exchange": "deribit",
"symbol": "BTC-PERPETUAL",
"params": { "which": "all" }
}
Futures and options are independent
cancel only affects algo loops on the named exchange. Options positions are exchange-held positions, not algo loops — use options_trim or CLEAN to close them. A cancel(which=mm_main) leaves all options positions untouched.
Anchor Price
When anchorPrice is present, store.setRegime() persists it to SQLite (bot_state table). It survives restarts.
| Component | Uses anchorPrice? | Behaviour |
|---|---|---|
| Dip Buyer / Ladder | Yes — primary | Anchor is the centre of the buy ladder. Set once on regime change; ladder rebuilds from stored value on restart. |
| Grid Maker | No | Always anchors to live mid at startup (self-centering). Pass explicit anchorPrice param to override. |
| Market Makers (main/fast/slow) | No | Centres on live mid/bid/ask at startup via ticker fetch. |
| Flow Maker | No | Centres on live mid at startup. anchorResetPct controls when the ladder is repriced relative to this internal anchor — not the stored anchorPrice. |
| Take Profit | No | Works off current position and mark price only. |
| Hedge | No | Uses live spot from ticker. |
anchorPrice, then fire the components. Or combine both in one payload — regime + signal in the same POST.
Grid Maker
Anchor-based startup cleanup + symmetric flip-flop. On each start:
- Cancels all existing orders on the symbol (clean slate)
- Places a buy ladder from
anchor − spacingdown toanchor × (1 − buyDepthPct) - Reads live position; places sell ladder above anchor based on
floor((position − minPosition) / perLevel), capped to buy level count - Enters flip-flop: buy fill → sell pong; sell fill → buy pong; pong fill → new ping
| Param | Default | Description |
|---|---|---|
anchorPrice | 0 | 0 = use live mid at startup (self-centering) |
buyDepthPct | 4 | % below anchor for bottom of buy ladder |
spacingPct | 0.1 | % between levels |
allocation | 10 | Total USD across all buy levels (snapped to $10 increments) |
pongMinOffset | 0.05% | Min distance from fill price to pong (% or absolute) |
minPosition | 0 | Floor — sell pongs skipped if at/below this. Redundant if reduce_only is active (it is). |
sellInLoss | true | Allow sell pongs below avg entry price |
cancelOnFillPause | 0 | ms to wait after buy fill before placing sell pong (adverse-move guard) |
cancelOnFillPauseThreshold | 0 | Minimum adverse move % required to skip pong during the pause window. 0 = skip on any adverse tick. Example: 0.1 = only skip if price moves >0.1% against the fill. |
driftThresholdPct | 0 | Auto-reprice when mid moves this % from anchor. 0 = disabled. Example: 3 = reprice if price drifts >3%. |
repriceCooldownMins | 60 | Minimum minutes between automatic reprices |
Flow Maker
A five-layer market maker designed to move with price, capture edge on both sides, and build inventory asymmetrically in dips. Bid and ask ladders are fully independent — a skipped or failed ask never drains buy-side count.
Layer 1 — Geometry
Places orderCount bids below mid and orderCount asks above mid, separated by a spread dead zone. The total allocation is split between sides by bidFraction (default 0.7 = 70% bids). Spacing defaults to spread ÷ orderCount but can be set independently per side.
Layer 2 — Price following
Every priceFollowSecs, checks whether price has drifted away from the nearest resting order. If the gap exceeds the threshold, the deepest order on that side is cancelled and replaced one step closer to mid — walking the ladder back toward price. Asks follow aggressively (low threshold); bids follow lazily (high threshold) so they stay deep to catch dips.
Layer 3 — Dip inventory
dipScaleFactor weights bid order sizes so deeper levels carry proportionally larger lots. At dipScaleFactor=1 the deepest bid is 2× the nearest bid; at 0 all levels are equal. Total allocation is always preserved. Extension orders (placed after a fill) use the maximum multiplier since they are always the deepest point.
Layer 4 — Signal edge
Three optional signals shift placement or pull quotes:
- Imbalance skew — reads the order book and shifts the placement mid up or down based on bid/ask volume ratio. Reduces adverse selection.
- Momentum gate — if mid moves more than
momentumThresholdPctinmomentumWindowSecs, all orders are cancelled, the bot pauses, then re-enters at the new price. - Vol-scaled spread —
baseVolatilitysets a vol baseline; realised vol widens or narrows the spread automatically (0.5×–3.0× multiplier).
Layer 5 — Take-profit pongs
When pongOnFill=true, each bid fill places a limit sell at fill price + pongOffset. If price subsequently drops more than pongCancelDistance below the fill price, the pending pong is cancelled — protecting against round-trip losses on adverse moves.
Profit gate (Mode 3 sideline)
When profitSidelineThreshold is set, flow maker monitors the position's unrealised P&L as a percentage above the average entry price ((mid − avgEntry) / avgEntry). If this exceeds the threshold, all orders — bids, asks, and pongs — are immediately cancelled and no new orders are placed. The bot idles until price retreats back to or below the threshold, at which point the full ladder is re-established at current mid and normal operation resumes.
The intent is to sideline flow when main already has a profitable position to distribute. Flow re-buying at elevated prices would push average entry higher and crowd out main's natural distribution. When the gate clears the ladder re-anchors fresh, so any re-accumulation starts at the new (lower) price.
If avgEntryPrice is unavailable (e.g. no open position) the gate is skipped and flow runs normally. Mid-price is used as the P&L reference — accurate to within a few basis points of Deribit mark price under normal conditions.
Params reference
| Param | Default | Description |
|---|---|---|
allocation | — | Total contracts (% or abs). Split between bid/ask sides by bidFraction. |
bidFraction | 0.7 | 0–1. Fraction of allocation assigned to bids. Remainder goes to asks. |
orderCount | 5 | Levels per side. |
spread | — | Inner dead zone between best bid and best ask (% or abs USD). |
bidStep | 0 | Spacing between bid levels. 0 = spread ÷ orderCount. |
askStep | 0 | Spacing between ask levels. 0 = spread ÷ orderCount. |
dipScaleFactor | 0 | 0–2. Multiplies bid lot size by depth. 0 = flat sizing. 1 = deepest bid is 2× nearest. |
positionStepScale | 0 | 0–1. Widens bidStep as position grows toward maxPosition. Slows accumulation at the ceiling without a hard stop. |
maxPosition | 0 | Hard long ceiling. Bid extension pauses when position ≥ this. 0 = no ceiling. |
maxFlowDistance | 0 | Max % below anchor the bid frontier may extend. 0 = no limit. |
anchorResetPct | 0 | Reprice the full ladder when mid recovers within X% of the internal anchor. 0 = never. |
priceFollowSecs | 30 | Seconds between price-follow shuffle checks. |
bidFollowThreshold | 0.5 | % gap between mid and nearest bid that triggers a bid shuffle. Higher = lazier following. |
askFollowThreshold | 0.15 | % gap between nearest ask and mid that triggers an ask shuffle. Lower = more aggressive following. |
baseVolatility | 0 | Annualised vol baseline for spread scaling e.g. 0.80. 0 = static spread. |
imbalanceSkewFactor | 0 | 0–1. Shift placement mid based on order-book bid/ask volume imbalance. |
imbalanceDepth | 10 | Order book levels per side to aggregate for imbalance calculation. |
momentumThresholdPct | 0 | % mid move over the window that pulls all orders. 0 = disabled. |
momentumWindowSecs | 30 | Rolling window for momentum measurement. |
momentumPauseSecs | 30 | Seconds to wait before re-entering after a momentum pull. |
pongOnFill | false | Place a take-profit ask above each bid fill. |
pongOffset | 0.15% | % or abs above fill price for the pong ask. |
pongCancelDistance | 0.3% | % price drop below fill price that cancels a pending pong. |
sellFloor | 0 | Suppress ask orders when position ≤ this level. Accepts % of equity (e.g. 10%) or absolute USD contracts. 0 = always show asks. |
fundingGatePct | 0 | Suspend bids when 8h funding ≤ this %. e.g. -0.05. 0 = disabled. |
profitSidelineThreshold | 0 | Pull all orders (bids, asks, pongs) when position is this % above avg entry price. e.g. 1%. Bot idles until price retreats below threshold, then re-engages with a fresh ladder. 0 = disabled. |
flowRegimes | "" | Comma-separated regimes where bids are active. Bids cancelled outside; restored on re-entry. Blank = all regimes. |
regimeSpreadMultipliers | {} | JSON map of regime → spread multiplier, applied on top of base spread. e.g. {"CALM":"1.0","TRANSITION":"1.3","STRESS":"2.0"}. Blank or {} = disabled. |
Example payloads
Start flow maker on its own:
{
"signal": "flow_mm",
"exchange": "deribit",
"symbol": "BTC-PERPETUAL",
"name": "test-bot"
}
Start alongside fast MM in one payload:
{
"signal": "fast_mm,flow_mm",
"exchange": "deribit",
"symbol": "BTC-PERPETUAL",
"name": "test-bot"
}
Cancel and restart flow maker alone (e.g. after a config change):
{
"signal": "cancel,flow_mm",
"exchange": "deribit",
"symbol": "BTC-PERPETUAL",
"name": "test-bot",
"params": { "which": "flow_maker" }
}
Market Makers (main / fast / slow)
Ping-pong order book. Places a scaled buy ladder, watches fills, places sell pongs above each filled buy. Sell pong fills → new buy ping.
| Param | Default | Description |
|---|---|---|
allocation | — | Contracts per side (regime + options multipliers applied) |
spread | — | Total price width of the order book (% or abs) |
orderCount | — | Orders per side |
pongMinOffset | — | Min distance from fill to pong (% or abs) |
pongMaxOffset | — | Max distance — trailing pong trails between min and max |
minPosition | 0 | Floor long contracts — no buys placed when position is at or below this |
maxPosition | 0 | Ceiling long contracts — suppresses new buy pings when position ≥ this (0 = no ceiling) |
sellInLoss | true | Allow sell pongs below avg entry price |
inventorySkewFactor | 0 | Skew bid/ask sizes towards inventory balance. 0 = symmetric; 0.5 = moderate skew. |
targetPosition | 0 | Desired inventory level (USD notional). Skew leans towards this target. |
baseVolatility | 0 | Baseline annualised vol for spread scaling. 0 = static spread. |
fundingSkewFactor | 0 | Adjusts quotes in response to funding rate. Positive = lean short when funding high. |
maxQuoteLifetime | 0 | Cancel and repost orders older than this (e.g. "4h"). 0 = never expire. |
cancelOnFillPause | 0 | Wait N ms after a fill before placing the pong — checks for adverse price movement during the pause. 0 = disabled. |
cancelOnFillPauseThreshold | 0 | Minimum adverse move % required to skip pong. 0 = any move against fill triggers skip. Example: 0.1 = only skip if price moved >0.1% in the wrong direction. |
driftThresholdPct | 0 | % price drift from anchor before auto-reprice. 0 = disabled. |
repriceCooldownMins | 60 | Min minutes between auto-reprices. |
imbalanceSkewFactor | 0 | 0–1: shift mid based on order-book bid/ask volume imbalance. Reduces adverse selection on buys and sells. |
imbalanceDepth | 10 | Order book levels per side to aggregate for imbalance calculation. |
momentumThresholdPct | 0 | % mid move over the window that cancels all pings and pauses quoting. 0 = disabled. On UP moves, also cancels stale pong sells below avg entry, preventing sweep losses on rising price. |
momentumWindowSecs | 30 | Rolling window for momentum measurement. |
momentumPauseSecs | 30 | Seconds to wait before re-entering after a momentum pull. |
regimeSpreadMultipliers | {} | JSON map of regime → spread multiplier. Applied on top of base spread. e.g. {"CALM":"1.0","TRANSITION":"1.2","STRESS":"1.8"}. Blank or {} = disabled. |
portfolioSkewFactor | 0 | 0–1 strength of portfolio-level delta correction. Shifts mid when net portfolio delta is outside the target band. 0 = disabled. |
portfolioTargetDeltaMin | 0 | Minimum desired portfolio delta in BTC. Skew leans toward buying when below this. 0 = skew disabled. |
portfolioTargetDeltaMax | 0 | Maximum desired portfolio delta in BTC. Skew leans toward selling when above this. 0 = skew disabled. |
autoBalance | none | none or shuffle. Shuffle moves the backmost ping to one step ahead of the frontmost when price leaves the range, gradually walking the ladder with price. |
autoBalanceEvery | 60 | Seconds between shuffle checks. Each check moves at most one order. |
autoBalanceRegimes | "" | Comma-separated regimes where autoBalance is active, e.g. CALM. Empty = all regimes. Shuffle is suppressed automatically outside the listed regimes. |
sizeSkew | 1.0 | Buy-side size multiplier relative to sell. 1.0 = symmetric. 1.1 = buy layers are 10% larger than sell layers. Applied to initial placement, drift reprices, and repair refills. Min-position top-ups are unaffected. |
takeProfitMinPosition | 0 | Minimum position size (contracts) required to arm the passive TP order. 0 = disabled. |
takeProfitMinPnl | 0 | Minimum unrealised PnL in USD required to place a TP order. 0 = no PnL gate. |
takeProfitOffset | 50 | Distance above mid for the passive reduce-only limit sell. Accepts USD (e.g. 50) or percent (e.g. 0.1%). Reprices when drift exceeds 50 % of offset distance. |
takeProfitAmount | 0 | Contracts to offer per TP order. 0 = sell everything above sellFloor. |
takeProfitCheckEvery | 60 | Seconds between TP condition checks. The TP loop runs independently of the main quote loop. |
sellMapAnchor | false | Anchor the sell ladder to avg entry price (MAP) rather than mid. When true, sell orders start at MAP + fromSpread, so the ladder always demands profit relative to your average entry regardless of where mid is. Falls back to mid when no position exists or MAP is unavailable. |
sellMinFromSpread | 0 | Minimum from-spread at max position when MAP anchoring is active (% or abs, e.g. 0.5%). Above sellCompressionThreshold, the from-spread linearly compresses from its base value (spread÷2) down to this — lowering profit demands at high inventory to reduce risk of ruin. 0 = no compression. |
sellMaxToSpread | 0 | Maximum sell ladder width (depth) at 0% position (% or abs, e.g. 3%). Combined with sellMinToSpread for an inverse-linear to-spread: wide at low position to catch fat tails, narrow at max position for fast distribution. 0 = use static effectiveDepth. |
sellMinToSpread | 0 | Minimum sell ladder width at max position (% or abs, e.g. 1.5%). Example: sellMaxToSpread=3%, sellMinToSpread=1.5% → 3% range at 0% position narrowing to 1.5% at 100% position. |
sellCompressionThreshold | 0.8 | position ÷ maxPosition ratio above which from-spread starts to compress AND the headroom cap is bypassed (allowing the full sell order count). 0.8 = triggers when position exceeds 80% of maxPosition. |
sellBreachRepriceMins | 5 | When position ≥ sellCompressionThreshold, force a sell ladder reprice if the ladder has been static for this many minutes — without waiting for the normal drift or quote-lifetime cycle. Keeps MAP-anchored sells fresh during rapid inventory build. 0 = disabled. |
Dip Buyer
Places a ladder of trailing limit buy orders below current price, each spaced by toOffset. Uses stored anchorPrice as the ladder centre. Each fill is independent — no pong placed.
Sell Ladder tp / sell_ladder
Ratcheting partial TP monitor. Watches position and mark price; places scaled trailing-limit sell orders when price crosses TP thresholds. Resets on new position entry.
Threshold TP threshold_tp
Position-size watcher. When position exceeds threshold, sells sellPct% at market. No price triggers — just size-based trimming. Use when the grid has accumulated too large a position and you want to scale back automatically.
| Param | Default | Description |
|---|---|---|
threshold | 200 | Position size (USD contracts) that triggers a sell |
sellPct | 25 | % of total position to sell per trigger |
minPosition | 0 | Floor — never sell below this |
cooldownMins | 60 | Minutes between consecutive sells |
checkInterval | 60 | Seconds between position polls |
tag | threshold_tp | Order tag prefix |
Min Order
Single permanent anchor bid — a standing buy at a fixed level that refreshes after each fill. Ensures a baseline position is always being rebuilt even when all other components are idle.
Hedge (Options)
Opens an options position (put or call) sized as a % of equity. Strength 1–3 controls strike selection (ATM → OTM). Uses live spot from ticker unless overridden.
Circuit Breaker
Intra-regime shock protection that activates regardless of the current regime. Monitors a rolling price window per exchange:symbol. If price drops more than dropPct% within windowMins minutes, the system immediately forces BLACK regime and calls cancelAll() — cancelling every open order across all bots. Does not re-trigger if already in BLACK.
| Config key | Description |
|---|---|
circuitBreaker.dropPct | % drop that triggers the breaker (e.g. 5 = 5% decline) |
circuitBreaker.windowMins | Rolling window in minutes (e.g. 15) |
Configured in config/local.json under "circuitBreaker". Because it forces BLACK, the regime-overrides table then controls which components (if any) are allowed to restart.
Momentum Gate
Per-component protection inside market maker and flow maker. When the mid-price moves more than momentumThresholdPct% over momentumWindowSecs seconds, all quotes for that component are cancelled and the component pauses for momentumPauseSecs seconds before re-entering. This fires within any regime — including CALM — without waiting for a regime transition signal.
| Param | Description |
|---|---|
momentumThresholdPct | Mid move % that triggers the gate (e.g. 0.5) |
momentumWindowSecs | Lookback window to measure the move (e.g. 45) |
momentumPauseSecs | Seconds quotes stay pulled after trigger (e.g. 30) |
The pause is cancellation-aware — if the bot is stopped while pausing, the wait exits immediately.
Adverse Selection Guard
Protects against being run over by informed flow. After a fill, the fast market maker checks whether the post-fill price moved adversely by more than cancelOnFillPause ms worth of drift. If so, pong placement is skipped (or delayed). cancelOnFillPauseThreshold sets the number of consecutive adverse fills before the entire component pauses.
| Param | Description |
|---|---|
cancelOnFillPause | Ms to wait after a fill before placing the pong — detects if price moved adversely |
cancelOnFillPauseThreshold | Minimum adverse move % required to skip the pong during the pause window. 0 = skip on any adverse tick. Example: 0.1 = only skip if price moved >0.1% in the wrong direction. |
Risk Limits
Global position and drawdown guards configured in config/local.json under "riskLimits". Enforced across all bots and components.
| Config key | Description |
|---|---|
riskLimits.maxPositionPerSymbol | Max gross USD exposure per symbol (e.g. 50000) |
riskLimits.maxTotalPosition | Max gross USD exposure across all symbols (e.g. 150000) |
riskLimits.maxDrawdownPct | Max drawdown % from equity peak before all components halt (e.g. 15) |
Regime States
| Regime | Description | Typical components |
|---|---|---|
CALM | Low vol, range-bound | All components active |
TRANSITION | Breakout or breakdown forming | Grid + fast MM, TP active |
STRESS | Elevated vol, directional move | Min order + TP only |
BLACK | Extreme event / flash crash | Hedge only or all stopped |
AFTERMATH | Post-event recovery | Dip buyer + grid to rebuild |
Each component has an activeInRegimes array in config. When start_bot fires, only components whose regimes include the current stored regime are started.
Vol Classifier Config
Automatic regime detection from realised vol + funding + drawdown. Set enabled: true to activate. See Runbooks → Regime Classifier for operational detail.
| Key | Default | Description |
|---|---|---|
enabled | false | Enable automatic regime detection |
symbol | BTC-PERPETUAL | Instrument to fetch candles for |
candleResolution | "60" | Candle size in minutes |
candleCount | 48 | Candles fetched per poll |
overridePriority | false | If true, classifier can override manually set regime |
deescalateAfterPolls | 3 | Consecutive polls below threshold before downgrading |
thresholds.calm.maxVol | 0.60 | Max annualised vol for CALM |
thresholds.transition.maxVol | 1.00 | Max annualised vol for TRANSITION |
thresholds.stress.maxVol | 1.50 | Max annualised vol for STRESS |
blackVol | 1.50 | Vol threshold for BLACK (must also meet blackDrawdown) |
blackDrawdown | 0.15 | Drawdown threshold for BLACK (15% from recent HWM) |
extremeFundingRate | 0.0004 | 8h rate that escalates to STRESS regardless of vol |
Options Multipliers
A second layer of buy/sell multipliers that stack on top of regime multipliers when an options position is open.
"optionsMultipliers": {
"put": { "buyMultiplier": 1.2, "sellMultiplier": 0.8 },
"call": { "buyMultiplier": 0.8, "sellMultiplier": 1.2 },
"none": { "buyMultiplier": 1.0, "sellMultiplier": 1.0 }
}
Effective multiplier = regime_mult × options_mult. A zero from the regime side is never un-gated.
Convexity Metrics Reference
Computed from portfolio_daily (90-day window, refreshed every 15 min). Carry is taken from the carry_cost column when present, otherwise from fees + funding.
| Metric | Target | Formula | Description |
|---|---|---|---|
RCG | >3.0 | PnL / carry | Realized Convex Gains — how many times over you earned your carry costs |
UTR | <0.30 | days below HWM / days | Underwater Time Ratio — fraction of days equity was below all-time high |
CDI | <0.05 | carry / (avgEquity × days) | Carry Drag Index — daily carry as fraction of equity |
CPC | >0.50 | top-20% days / all positive days | Convex Payoff Concentration — how concentrated the gains are in the best days |
CPS | >3.0 | RCG × (1−UTR) × e−CDI | Composite Performance Score — blended rank. Primary sort key. |
Health Index | >0.70 | weighted blend | 0–1 summary. ≥0.70 HIB · ≥0.50 MIB · <0.50 LIB |
portfolioReturn | — | (end−start) / start | Total return over the query window |
relativeReturn | — | portfolioReturn − btcReturn | Alpha vs BTC buy-and-hold over same period. null if BTC price data unavailable. |
dailyCompoundRate | — | (1 + r)^(1/days) − 1 | Equivalent to the Excel/Sheets RATE formula. Daily compounding rate implied by the period return. |
annualisedReturn | — | (1 + dcr)^365 − 1 | Annualised return extrapolated from daily compound rate. |
Component Sharpe: annualised Sharpe per bot tag prefix. Zero-padded calendar days. formula: mean / std × √365.
Adverse Selection Score: −mean(signed post-fill return at horizon). Positive = being picked off. On-demand, requires Deribit candle fetch.
Backtest CLI
Run historical simulations from the command line against Deribit candle data.
node src/backtest/cli.js --strategy grid --days 30 --alloc 1000 --pong 0.2 node src/backtest/cli.js --strategy grid --days 60 --grid pong=0.1:0.2:0.3,spacing=0.05:0.1:0.2
Global flags
| Flag | Default | Description |
|---|---|---|
--symbol | BTC-PERPETUAL | Instrument |
--resolution | 60 | Candle resolution in minutes |
--days | 30 | Days of history to fetch |
--balance | 1.0 | Starting balance in BTC |
--strategy | dip | dip or grid |
--grid | — | Parameter sweep: param=v1:v2:v3,param2=v1:v2 |
--regime | — | Path to regime CSV with columns ts,regime |
--testnet | off | Use testnet candle data |
--verbose | off | Print progress every 100 candles |
Dip strategy flags --strategy dip
| Flag | Default | Description |
|---|---|---|
--dip | 0.005 | Buy at N% below candle close |
--tp | 0.010 | Take profit at N% above close |
--size | 10 | Order size in USD |
Grid strategy flags --strategy grid
| Flag | Default | Description |
|---|---|---|
--anchor | 0 | Anchor price — 0 = first candle close |
--depth | 4 | Buy ladder depth in % |
--spacing | 0.1 | % spacing between levels |
--alloc | 1000 | Total USD allocation across all buy levels |
--pong | 0.1 | Ping-pong spread: % above buy fill for sell pong (below sell fill for buy pong) |
Output metrics
| Metric | Description |
|---|---|
RCG | Realized Convex Gains — realized PnL relative to carry cost. Higher is better. |
CPS | Composite Performance Score — blended rank across all metrics. Primary sort key. |
UTR | Underwater Ratio — fraction of candles where equity was below the high-water mark. Lower is better. |
CDI | Carry Drag Index — implied carry cost relative to gains. Lower is better. Note: options premium not modelled. |
CPC | Convex Payoff Concentration — how concentrated gains are in the best candles. Higher signals convexity. |
API Endpoints
| Method | Path | Description |
|---|---|---|
POST | /api/webhook | Main signal entry point |
GET | /api/health | Process health — uptime, regime, activeBots, dbOk, stagingMode |
GET | /api/state | Full state snapshot (JSON) |
GET | /api/bot-config | Read bot config from local.json |
POST | /api/bot-config | Write bot config to local.json |
POST | /api/restart | Restart process via pm2 |
GET | /api/consistency-check | Check for orphaned orders |
POST | /api/cancel-bot-orders | Cancel all open orders for a bot |
GET | /api/metrics?days=N | Convexity metrics from portfolio_daily (also triggers WS push) |
GET | /api/regime-changes?limit=N | Regime change log with vol, funding, outcomePct |
GET | /api/fills-heatmap?days&bucket&tag | Fill count/volume bucketed by price level, per side |
GET | /api/component-sharpe?days=N | Annualised Sharpe per bot component (tag prefix) |
GET | /api/adverse-selection?days&horizonHours&symbol | Adverse selection score — requires Deribit candle fetch (~1s) |
GET | /api/equity-history?hours=N | Equity snapshots for chart zoom (max 720h) |
Options — Open Put / Call
Send a structured JSON signal to POST /trade. The server resolves the best instrument (strike + expiry), sizes the position, and places a limit buy on Deribit.
Long put — open downside protection:
{
"signal": "options_long_put",
"exchange": "deribit",
"symbol": "BTC-USDC",
"params": {
"strike": 75000, // target strike in USD
"size": 1.2, // % of portfolio equity
"strength": 2 // 1=30 DTE, 2=37 DTE, 3=45 DTE (default: 2)
}
}
Long call — open upside participation:
{
"signal": "options_long_call",
"exchange": "deribit",
"symbol": "BTC-USDC",
"params": {
"strike": 92000,
"size": 0.8,
"strength": 1
}
}
Using OTM % instead of a fixed strike — server derives strike from live spot:
{
"signal": "options_long_put",
"exchange": "deribit",
"symbol": "BTC-USDC",
"params": {
"otm": 8.5, // spot × (1 − 0.085) → strike
"size": 1.2,
"strength": 2
}
}
If spot is omitted the server fetches the live BTC-PERPETUAL mark price automatically. If both strike and otm are supplied, strike wins.
Options — Trim
Partially closes positions that have moved ITM (|Δ| > 0.50). OTM positions are skipped.
{
"signal": "options_trim",
"exchange": "deribit",
"symbol": "BTC-USDC",
"params": {
"pct": 30 // % of each eligible position to close — typically 30 or 50
}
}
Options — CLEAN / Close
There is no JSON signal for a full cycle close. Send the text format to the same endpoint with a message field:
{
"message": "CLEAN | recycle capital"
}
Or via curl:
curl -X POST https://YOUR_DOMAIN/trade \ -d "message=CLEAN | recycle capital"
Behaviour: closes positions with |Δ| ≥ 0.03 at mark price, lets near-worthless positions expire, resets cycle state, and arms the +20% recycling boost on the next open signal.
Options — Params Reference
| Param | Signals | Required | Default | Description |
|---|---|---|---|---|
strike | open | one of | — | Target strike in USD |
otm | open | one of | — | OTM distance %; server derives strike from spot |
spot | open | no | auto-fetched | Current BTC price; omit to let server fetch live mark |
size | open | yes | — | Position size as % of portfolio equity (e.g. 1.2 = 1.2%) |
strength | open | no | 2 | Convexity strength 1–3; sets target DTE (30 / 37 / 45 days) |
currency | open | no | BTC | Settlement currency: BTC or BTC_USDC |
deltaTargetMin | open | no | 0 | Lower bound of desired portfolio delta band. When net delta is below this and a call signal fires, size is bumped to cover the undershoot back to band midpoint. |
deltaTargetMax | open | no | 1 | Upper bound of desired portfolio delta band. When net delta exceeds this and a put signal fires, size is bumped to cover the overshoot back to band midpoint. Recommended: 2.5 for a long-biased book. |
pct | trim | yes | — | % of eligible positions to close (typically 30 or 50) |
delta_floor | trim | no | 0.50 | Min |Δ| threshold — positions below this are skipped |
Server-side multipliers (clustering +25–50%, recycling +20%) are applied automatically — no params needed.
Deploy Workflow
GitHub is the relay between your Mac and the VPS. Nothing goes directly between them.
Local Mac ──push──▶ GitHub ──pull──▶ VPS (pm2)
Every deploy is three steps, always in this order:
# 1. Commit locally git add <file> && git commit -m "describe change" # 2. Push to GitHub git push # 3. On the VPS — pull and restart git pull && pm2 restart megadrive
If the VPS says "Already up to date" it means the push from your Mac hasn't happened yet — check that step 2 succeeded.
| Task | Command | Where |
|---|---|---|
| Deploy code | git pull && pm2 restart megadrive | VPS |
| Live logs | pm2 logs megadrive | VPS |
| Recent log history | pm2 logs megadrive --lines 200 | VPS |
| Bot status | pm2 list | VPS |
SSH & Keys
| Key | Where | Used for | Passphrase |
|---|---|---|---|
~/.ssh/id_ed25519 | Local Mac | Pushing to GitHub | Yes — must be loaded into agent |
~/.ssh/megadrive_deploy | Mac + VPS | VPS pulling from GitHub | No — works unattended |
If git push fails with "Permission denied" on your Mac, the SSH agent lost its keys (happens after reboot). Fix:
ssh-add ~/.ssh/megadrive_deploy # no passphrase — instant
git push
To make this permanent, add to ~/.ssh/config:
Host github.com IdentityFile ~/.ssh/megadrive_deploy AddKeysToAgent yes