Praeger Reuchlin
Signal Reference

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

signaldispatchesnotes
start_botall regime-gated componentsNormal operating mode — respects activeInRegimes per component
gridGrid MakerCancel existing → buy ladder → sell ladder from position → flip-flop
main_mmMain Market MakerWide spread, larger allocation
fast_mmFast Market MakerTight spread, active book
slow_mmSlow Market MakerWide spread, patient fills
flow_mmFlow MakerLayered MM with price following and dip accumulation — runs alongside or independently of other MMs
dip_buyerDip BuyerLaddered trailing limit buys below anchor
min_orderMin OrderSingle permanent anchor bid
hedgeHedge (options)Buys put/call protection
tp / sell_ladderSell LadderRatcheting partial TP ladder — price-triggered trailing sells
threshold_tpThreshold TPMarket sell X% of position when position > threshold
ladderScaled OrderOne-shot scaled entry — params forwarded directly
min_sizeLimit OrderSingle limit order — params forwarded
options_long_callOptions Open (call)
options_long_putOptions Open (put)
cancelCancel 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_allCancel everythingStops 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

whichEffect
mm_mainStops the main MM loop (tag = main)
mm_fastStops the fast MM loop (tag = fast)
mm_slowStops the slow MM loop (tag = slow)
gridStops the grid loop
dip_buyerStops the dip buyer loop
hedgeStops the hedge loop
buyStops all algo loops currently on the buy side
sellStops all algo loops currently on the sell side
allStops 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.

The new instance starts before the outgoing loop has physically pulled its orders (~a few seconds overlap). Each instance has its own internal ID so they don't interfere.

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.

ComponentUses 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.
On every new regime: fire a regime-only payload first to update 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:

ParamDefaultDescription
anchorPrice00 = use live mid at startup (self-centering)
buyDepthPct4% below anchor for bottom of buy ladder
spacingPct0.1% between levels
allocation10Total USD across all buy levels (snapped to $10 increments)
pongMinOffset0.05%Min distance from fill price to pong (% or absolute)
minPosition0Floor — sell pongs skipped if at/below this. Redundant if reduce_only is active (it is).
sellInLosstrueAllow sell pongs below avg entry price
cancelOnFillPause0ms to wait after buy fill before placing sell pong (adverse-move guard)
cancelOnFillPauseThreshold0Minimum 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.
driftThresholdPct0Auto-reprice when mid moves this % from anchor. 0 = disabled. Example: 3 = reprice if price drifts >3%.
repriceCooldownMins60Minimum 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:

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

ParamDefaultDescription
allocationTotal contracts (% or abs). Split between bid/ask sides by bidFraction.
bidFraction0.70–1. Fraction of allocation assigned to bids. Remainder goes to asks.
orderCount5Levels per side.
spreadInner dead zone between best bid and best ask (% or abs USD).
bidStep0Spacing between bid levels. 0 = spread ÷ orderCount.
askStep0Spacing between ask levels. 0 = spread ÷ orderCount.
dipScaleFactor00–2. Multiplies bid lot size by depth. 0 = flat sizing. 1 = deepest bid is 2× nearest.
positionStepScale00–1. Widens bidStep as position grows toward maxPosition. Slows accumulation at the ceiling without a hard stop.
maxPosition0Hard long ceiling. Bid extension pauses when position ≥ this. 0 = no ceiling.
maxFlowDistance0Max % below anchor the bid frontier may extend. 0 = no limit.
anchorResetPct0Reprice the full ladder when mid recovers within X% of the internal anchor. 0 = never.
priceFollowSecs30Seconds between price-follow shuffle checks.
bidFollowThreshold0.5% gap between mid and nearest bid that triggers a bid shuffle. Higher = lazier following.
askFollowThreshold0.15% gap between nearest ask and mid that triggers an ask shuffle. Lower = more aggressive following.
baseVolatility0Annualised vol baseline for spread scaling e.g. 0.80. 0 = static spread.
imbalanceSkewFactor00–1. Shift placement mid based on order-book bid/ask volume imbalance.
imbalanceDepth10Order book levels per side to aggregate for imbalance calculation.
momentumThresholdPct0% mid move over the window that pulls all orders. 0 = disabled.
momentumWindowSecs30Rolling window for momentum measurement.
momentumPauseSecs30Seconds to wait before re-entering after a momentum pull.
pongOnFillfalsePlace a take-profit ask above each bid fill.
pongOffset0.15%% or abs above fill price for the pong ask.
pongCancelDistance0.3%% price drop below fill price that cancels a pending pong.
sellFloor0Suppress ask orders when position ≤ this level. Accepts % of equity (e.g. 10%) or absolute USD contracts. 0 = always show asks.
fundingGatePct0Suspend bids when 8h funding ≤ this %. e.g. -0.05. 0 = disabled.
profitSidelineThreshold0Pull 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.

ParamDefaultDescription
allocationContracts per side (regime + options multipliers applied)
spreadTotal price width of the order book (% or abs)
orderCountOrders per side
pongMinOffsetMin distance from fill to pong (% or abs)
pongMaxOffsetMax distance — trailing pong trails between min and max
minPosition0Floor long contracts — no buys placed when position is at or below this
maxPosition0Ceiling long contracts — suppresses new buy pings when position ≥ this (0 = no ceiling)
sellInLosstrueAllow sell pongs below avg entry price
inventorySkewFactor0Skew bid/ask sizes towards inventory balance. 0 = symmetric; 0.5 = moderate skew.
targetPosition0Desired inventory level (USD notional). Skew leans towards this target.
baseVolatility0Baseline annualised vol for spread scaling. 0 = static spread.
fundingSkewFactor0Adjusts quotes in response to funding rate. Positive = lean short when funding high.
maxQuoteLifetime0Cancel and repost orders older than this (e.g. "4h"). 0 = never expire.
cancelOnFillPause0Wait N ms after a fill before placing the pong — checks for adverse price movement during the pause. 0 = disabled.
cancelOnFillPauseThreshold0Minimum 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.
driftThresholdPct0% price drift from anchor before auto-reprice. 0 = disabled.
repriceCooldownMins60Min minutes between auto-reprices.
imbalanceSkewFactor00–1: shift mid based on order-book bid/ask volume imbalance. Reduces adverse selection on buys and sells.
imbalanceDepth10Order book levels per side to aggregate for imbalance calculation.
momentumThresholdPct0% 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.
momentumWindowSecs30Rolling window for momentum measurement.
momentumPauseSecs30Seconds 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.
portfolioSkewFactor00–1 strength of portfolio-level delta correction. Shifts mid when net portfolio delta is outside the target band. 0 = disabled.
portfolioTargetDeltaMin0Minimum desired portfolio delta in BTC. Skew leans toward buying when below this. 0 = skew disabled.
portfolioTargetDeltaMax0Maximum desired portfolio delta in BTC. Skew leans toward selling when above this. 0 = skew disabled.
autoBalancenonenone 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.
autoBalanceEvery60Seconds 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.
sizeSkew1.0Buy-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.
takeProfitMinPosition0Minimum position size (contracts) required to arm the passive TP order. 0 = disabled.
takeProfitMinPnl0Minimum unrealised PnL in USD required to place a TP order. 0 = no PnL gate.
takeProfitOffset50Distance 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.
takeProfitAmount0Contracts to offer per TP order. 0 = sell everything above sellFloor.
takeProfitCheckEvery60Seconds between TP condition checks. The TP loop runs independently of the main quote loop.
sellMapAnchorfalseAnchor 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.
sellMinFromSpread0Minimum 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.
sellMaxToSpread0Maximum 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.
sellMinToSpread0Minimum 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.
sellCompressionThreshold0.8position ÷ 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.
sellBreachRepriceMins5When 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.

ParamDefaultDescription
threshold200Position size (USD contracts) that triggers a sell
sellPct25% of total position to sell per trigger
minPosition0Floor — never sell below this
cooldownMins60Minutes between consecutive sells
checkInterval60Seconds between position polls
tagthreshold_tpOrder 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 keyDescription
circuitBreaker.dropPct% drop that triggers the breaker (e.g. 5 = 5% decline)
circuitBreaker.windowMinsRolling 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.

ParamDescription
momentumThresholdPctMid move % that triggers the gate (e.g. 0.5)
momentumWindowSecsLookback window to measure the move (e.g. 45)
momentumPauseSecsSeconds 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.

ParamDescription
cancelOnFillPauseMs to wait after a fill before placing the pong — detects if price moved adversely
cancelOnFillPauseThresholdMinimum 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 keyDescription
riskLimits.maxPositionPerSymbolMax gross USD exposure per symbol (e.g. 50000)
riskLimits.maxTotalPositionMax gross USD exposure across all symbols (e.g. 150000)
riskLimits.maxDrawdownPctMax drawdown % from equity peak before all components halt (e.g. 15)

Regime States

RegimeDescriptionTypical components
CALMLow vol, range-boundAll components active
TRANSITIONBreakout or breakdown formingGrid + fast MM, TP active
STRESSElevated vol, directional moveMin order + TP only
BLACKExtreme event / flash crashHedge only or all stopped
AFTERMATHPost-event recoveryDip 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.

KeyDefaultDescription
enabledfalseEnable automatic regime detection
symbolBTC-PERPETUALInstrument to fetch candles for
candleResolution"60"Candle size in minutes
candleCount48Candles fetched per poll
overridePriorityfalseIf true, classifier can override manually set regime
deescalateAfterPolls3Consecutive polls below threshold before downgrading
thresholds.calm.maxVol0.60Max annualised vol for CALM
thresholds.transition.maxVol1.00Max annualised vol for TRANSITION
thresholds.stress.maxVol1.50Max annualised vol for STRESS
blackVol1.50Vol threshold for BLACK (must also meet blackDrawdown)
blackDrawdown0.15Drawdown threshold for BLACK (15% from recent HWM)
extremeFundingRate0.00048h 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.

MetricTargetFormulaDescription
RCG>3.0PnL / carryRealized Convex Gains — how many times over you earned your carry costs
UTR<0.30days below HWM / daysUnderwater Time Ratio — fraction of days equity was below all-time high
CDI<0.05carry / (avgEquity × days)Carry Drag Index — daily carry as fraction of equity
CPC>0.50top-20% days / all positive daysConvex Payoff Concentration — how concentrated the gains are in the best days
CPS>3.0RCG × (1−UTR) × e−CDIComposite Performance Score — blended rank. Primary sort key.
Health Index>0.70weighted blend0–1 summary. ≥0.70 HIB · ≥0.50 MIB · <0.50 LIB
portfolioReturn(end−start) / startTotal return over the query window
relativeReturnportfolioReturn − btcReturnAlpha vs BTC buy-and-hold over same period. null if BTC price data unavailable.
dailyCompoundRate(1 + r)^(1/days) − 1Equivalent to the Excel/Sheets RATE formula. Daily compounding rate implied by the period return.
annualisedReturn(1 + dcr)^365 − 1Annualised 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

FlagDefaultDescription
--symbolBTC-PERPETUALInstrument
--resolution60Candle resolution in minutes
--days30Days of history to fetch
--balance1.0Starting balance in BTC
--strategydipdip or grid
--gridParameter sweep: param=v1:v2:v3,param2=v1:v2
--regimePath to regime CSV with columns ts,regime
--testnetoffUse testnet candle data
--verboseoffPrint progress every 100 candles

Dip strategy flags --strategy dip

FlagDefaultDescription
--dip0.005Buy at N% below candle close
--tp0.010Take profit at N% above close
--size10Order size in USD

Grid strategy flags --strategy grid

FlagDefaultDescription
--anchor0Anchor price — 0 = first candle close
--depth4Buy ladder depth in %
--spacing0.1% spacing between levels
--alloc1000Total USD allocation across all buy levels
--pong0.1Ping-pong spread: % above buy fill for sell pong (below sell fill for buy pong)

Output metrics

MetricDescription
RCGRealized Convex Gains — realized PnL relative to carry cost. Higher is better.
CPSComposite Performance Score — blended rank across all metrics. Primary sort key.
UTRUnderwater Ratio — fraction of candles where equity was below the high-water mark. Lower is better.
CDICarry Drag Index — implied carry cost relative to gains. Lower is better. Note: options premium not modelled.
CPCConvex Payoff Concentration — how concentrated gains are in the best candles. Higher signals convexity.
RCG and CPS do not include options premium. CDI is understated when running a combined grid + options strategy — treat backtest metrics as directional signal, not absolute.

API Endpoints

MethodPathDescription
POST/api/webhookMain signal entry point
GET/api/healthProcess health — uptime, regime, activeBots, dbOk, stagingMode
GET/api/stateFull state snapshot (JSON)
GET/api/bot-configRead bot config from local.json
POST/api/bot-configWrite bot config to local.json
POST/api/restartRestart process via pm2
GET/api/consistency-checkCheck for orphaned orders
POST/api/cancel-bot-ordersCancel all open orders for a bot
GET/api/metrics?days=NConvexity metrics from portfolio_daily (also triggers WS push)
GET/api/regime-changes?limit=NRegime change log with vol, funding, outcomePct
GET/api/fills-heatmap?days&bucket&tagFill count/volume bucketed by price level, per side
GET/api/component-sharpe?days=NAnnualised Sharpe per bot component (tag prefix)
GET/api/adverse-selection?days&horizonHours&symbolAdverse selection score — requires Deribit candle fetch (~1s)
GET/api/equity-history?hours=NEquity 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

ParamSignalsRequiredDefaultDescription
strikeopenone ofTarget strike in USD
otmopenone ofOTM distance %; server derives strike from spot
spotopennoauto-fetchedCurrent BTC price; omit to let server fetch live mark
sizeopenyesPosition size as % of portfolio equity (e.g. 1.2 = 1.2%)
strengthopenno2Convexity strength 1–3; sets target DTE (30 / 37 / 45 days)
currencyopennoBTCSettlement currency: BTC or BTC_USDC
deltaTargetMinopenno0Lower 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.
deltaTargetMaxopenno1Upper 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.
pcttrimyes% of eligible positions to close (typically 30 or 50)
delta_floortrimno0.50Min |Δ| 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.

TaskCommandWhere
Deploy codegit pull && pm2 restart megadriveVPS
Live logspm2 logs megadriveVPS
Recent log historypm2 logs megadrive --lines 200VPS
Bot statuspm2 listVPS

SSH & Keys

KeyWhereUsed forPassphrase
~/.ssh/id_ed25519Local MacPushing to GitHubYes — must be loaded into agent
~/.ssh/megadrive_deployMac + VPSVPS pulling from GitHubNo — 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
Praeger Reuchlin
Building dreams