Market Adapter
The live signal layer for AMA-priced bots. It reads candles, computes the AMA center price, optionally writes dynamic weights, and creates recalc triggers when a bot accepts its first center, when the center moves far enough, or when whitelisted range-scaling slope drift requires a grid-bound reset. DEXBot2 starts and stops the adapter automatically when active AMA bots exist.
Contents
- Big Picture
- Quick Start
- Grid Price
- Grid Range Scaling
- Asymmetric Weight Shift
- Symmetric Weight Shift
- Trigger Threshold
- Settings and Overrides
- Off-Hour Idle Behavior
- Live Writes and Dry-Run
- Useful Commands
- Troubleshooting
- Related Tools
- Technical Reference
Which section do I need?
| If you want to… | Read this | Key command |
|---|---|---|
| Enable AMA pricing for a bot | Quick Start | dexbot bot → 2) Modify bot → 6) Adapter |
| Change how often the grid rebuilds | Trigger Threshold | edit AMA_DELTA_THRESHOLD_PERCENT |
| Tune buy/sell weight bias | Asymmetric Weight Shift | dexbot bot → 2) Modify bot → 6) Adapter → Weight |
| Widen/tighten grid bounds by trend | Grid Range Scaling | dexbot bot → 2) Modify bot → 6) Adapter → Range |
| Override settings for one pair or bot | Settings and Overrides | edit profiles/market_adapter_settings.json |
| Run the adapter standalone or test dry-run | Live Writes and Dry-Run | node dist/market_adapter/market_adapter.js --dryRun |
| Debug a bot not being processed | Troubleshooting | dexbot bot → 2) Modify bot → 6) Adapter flags |
| Understand the signal pipeline or module layout | Technical Reference | — |
Big Picture
The adapter feeds five separate grid controls from the same candle/AMA pipeline:
| Control | Signal | Effect |
|---|---|---|
| Grid price | AMA price | Moves the grid center through bootstrap and delta reset triggers |
| Grid range scaling | AMA slope | Widens the trend side, tightens the opposite bound through whitelisted slope reset triggers |
| Market price offset | AMA slope | Offsets the live market/start price through whitelisted slope reset triggers |
| Asymmetric weight shift | AMA slope + Kalman filter | Biases buy/sell allocation in the trend direction |
| Symmetric weight shift | Volatility | Reduces both buy and sell weights during noisy periods |
In short: AMA price sets the center; AMA slope scales the range and derives the market price offset; AMA plus Kalman drives directional weight bias; volatility applies the symmetric penalty.
Quick Start
1. Enable AMA
New bots default to gridPrice: "ama3". Change it in profiles/bots.json or through dexbot bot to ama, ama1, ama2, ama3, or ama4. ama1 reacts fastest, ama4 is the slowest and steadiest (higher numbers smooth more), and ama uses the pair's default preset.
startPrice selects the candle source:
startPrice |
Adapter source |
|---|---|
pool |
Liquidity pool price |
book |
Order book mid price (best bid/ask) |
| numeric value | Fixed anchor (skips candle fetching and SMA warmup); used directly as the static seed price |
When fetching candles (pool or book), the adapter requires a full historical window. The oldest erPeriod candles are used for an initial SMA (Simple Moving Average) warmup phase to seed the AMA and establish the first Efficiency Ratio (ER) calculation. See AMA Warmup Window for technical details.
2. Enable Live Writes
Open the bot editor — dexbot bot → 2) Modify bot → pick the bot → 6) Adapter — and set the three per-bot flags:
| Flag | Whitelist key | Effect |
|---|---|---|
| Price | ama |
Allows live dynamicgrid.json and recalc-trigger writes. Without it the adapter still computes state, but live output stays in dry-run. |
| Weight | dynamicWeight |
Opt-in dynamic buy/sell weights. |
| Range | asymmetricBounds |
Opt-in AMA-slope range scaling. |
The flags are stored per bot in profiles/market_adapter_whitelist.json; re-open the section any time to inspect or change them. Boolean prompts accept y/yes/true and n/no/false; Enter keeps the current value. Renaming a bot carries its flags to the new key, deleting a bot removes its entry, and all-off flags are saved as an explicit false entry so the flags never silently fall back to defaults.
3. Start DEXBot2
Start DEXBot2 normally. The bot runtime launches the adapter when needed.
Grid Price
Grid price is the AMA price used as the grid center. gridPrice: "ama" uses the pair's defaultAma from profiles/market_profiles.json. gridPrice: "ama1" through gridPrice: "ama4" force a specific preset. If no pair profile matches, the bot's ama block is used as the fallback.
Empirical Divergence Risk Management
The adapter uses tiered clamping thresholds to manage inventory risk during extreme price divergence from the AMA trend center. These thresholds are derived from historical pool volatility and replace static 'fit cap' multipliers. The specific clamping limits and exit parameters are calculated per pair and preset using the AMA fitting toolchain:
node dist/analysis/ama_fitting/optimizer_high_resolution.js --data <lp-file.json>Source: example LP pool, 1h candles, representative historical window.
| AMA preset | 99.9% — 3.29σ | 99.99% — 3.89σ | 99.999% — 4.42σ |
|---|---|---|---|
| AMA1 | 1.461x | 1.571x | 1.626x |
| AMA2 | 1.467x | 1.564x | 1.619x |
| AMA3 | 1.473x | 1.557x | 1.612x |
| AMA4 | 1.479x | 1.546x | 1.601x |
Divergence is calculated as abs(Price - AMA) / AMA. Limit exits use the selected historical divergence tier to protect the bot from extreme price excursions and prevent runaway inventory accumulation during high-divergence volatility.
Grid Range Scaling
AMA slope can scale the rebuilt grid in two linked ways under the same whitelist gate:
- tilt the configured min/max range toward the trend direction
- offset the live market/start price used for initial placement
This is separate from dynamic buy/sell weighting. Both grid-range effects are enabled only when asymmetricBounds: true is set in profiles/market_adapter_whitelist.json. Range scaling is opt-in: enable it per bot with dexbot bot → 2) Modify bot → 6) Adapter → Range.
Technical formula and tuning details are in Grid Range Scaling Model.
When range scaling is whitelisted, the adapter persists the accepted slope baseline in gridRangeScalingAmaSlope. A reset trigger is emitted only when the slope delta crosses the configured threshold; direction changes alone do not reset the grid.
Asymmetric Weight Shift
AMA slope plus Kalman filter confirmation can bias the bot's configured weightDistribution in the trend direction. In an uptrend or downtrend, this can shift allocation toward the side the strategy wants to emphasize while still starting from the bot's static buy/sell weights. See research guide.
Symmetric Weight Shift
Volatility can reduce both buy and sell weights during noisy periods. This is a shared penalty on both sides, separate from the directional AMA/Kalman bias. See research guide.
Both weight shifts are controlled separately from AMA pricing. Set dynamicWeight: false in profiles/market_adapter_whitelist.json to keep AMA pricing active but leave buy/sell weights static. Most users should leave the tuning values alone unless they are fitting or testing strategy parameters. Technical formula and tuning details are in Dynamic Weight Model.
Trigger Threshold
The adapter writes a recalc trigger when both conditions are true:
- the AMA center moved more than
AMA_DELTA_THRESHOLD_PERCENT - candle data is not stale
Configure the default in profiles/general.settings.json or from the dexbot bot general settings menu:
{
"MARKET_ADAPTER": {
"AMA_DELTA_THRESHOLD_PERCENT": 1
}
}Settings and Overrides
Market-adapter settings resolve in this order:
- Built-in defaults in
modules/constants.ts - Global overrides in
profiles/general.settings.jsonordexbot bot - Pair-specific overrides in
profiles/market_profiles.json - Bot-specific overrides in
profiles/market_adapter_settings.json
Data and State Location
The adapter keeps candle data and runtime state in a layout that follows where the profiles dir resolves (~/.config/dexbot2/profiles by default for all installs; a source checkout keeps <install-root>/profiles while a populated profiles dir exists there — bots.json, keys.json, settings, or launcher config):
| Install | Data / state dir |
|---|---|
| Source checkout with repo profiles | <install-root>/market_adapter/data and <install-root>/market_adapter/state |
| Global npm install / fresh source checkout | <profiles>/market_adapter/data and <profiles>/market_adapter/state, where <profiles> defaults to ~/.config/dexbot2/profiles |
npm packages ship only compiled dist/market_adapter/, so state is relocated under the profiles dir instead of being written into the package dir (which npm updates wipe and which may be read-only). Both can be overridden:
export DEXBOT_MARKET_ADAPTER_DATA_DIR=/custom/candle-data
export DEXBOT_MARKET_ADAPTER_STATE_DIR=/custom/adapter-stateThe dexbot clear-market-adapter / dexbot clear-all commands and the scripts/clear-*.sh helpers resolve the same dirs (see scripts/README.md).
Global Overrides
Override any MARKET_ADAPTER constant by adding a matching key under MARKET_ADAPTER in profiles/general.settings.json:
{
"MARKET_ADAPTER": {
"AMA_DELTA_THRESHOLD_PERCENT": 2.0,
"DYNAMIC_WEIGHT_CLIP_PERCENTILE": 5
}
}Pair-Specific Overrides
Pair-specific AMA profiles live in profiles/market_profiles.json:
{
"version": 1,
"profiles": [{
"key": "EXAMPLE-BOT",
"assetA": "IOB.XRP",
"assetB": "BTS",
"intervalSeconds": 3600,
"defaultAma": "AMA1",
"amas": {
"AMA1": { "erPeriod": 200, "fastPeriod": 3, "slowPeriod": 80 },
"AMA2": { "erPeriod": 500, "fastPeriod": 5, "slowPeriod": 120 }
}
}]
}profiles/market_adapter_settings.json layers:
{
"globals": {
"deltaThresholdPercent": 2.0,
"regimeSensitivity": 1.0
},
"pairs": [{
"assetASymbol": "IOB.XRP",
"assetBSymbol": "BTS",
"marketAdapterSettings": {
"deltaThresholdPercent": 3.0,
"amaSlope": { "maxSlopePct": 1.2 }
}
}]
}globals— applies to every market and botpairs[].marketAdapterSettings— overrides one market pair
Per-Bot Overrides
botOverrides is a general per-bot override within each pair entry. It applies at the highest priority and can hold any supported field:
{
"pairs": [{
"assetASymbol": "IOB.XRP",
"assetBSymbol": "BTS",
"botOverrides": {
"EXAMPLE-BOT": {
"deltaThresholdPercent": 4.0,
"defaultAmaKey": "AMA1"
}
}
}]
}Off-Hour Idle Behavior
The adapter only has work once per closed candle, so it stays quiet in between:
| Phase | Behavior |
|---|---|
| Startup | Sleep-first, before any connection. A respawned daemon checks whether every active AMA bot already consumed the newest closed candle; if so it sleeps to the next pollSeconds boundary (default 3600) without opening a socket, otherwise a catch-up cycle runs immediately. |
| Per bot, per cycle | Closed-bucket gate before any fetch. If the bot already consumed the newest closed bucket and its candle cache covers it (right source, no unresolved gaps, warmup target met), the bot is skipped before the native overlap fetch, the Kibana stale-tail check, and the candle re-serialize. |
| Between cycles | The BitShares socket is torn down after every cycle; the process sleeps with only an unref'd lock heartbeat. |
Both gates fail open: any input they cannot evaluate — fresh or unusable state, an interval that differs from the poll cadence (e.g. 2h candles polled hourly), a marker ahead of the clock (clock skew), an active bot with no row or no consumed marker (bootstrap owed), a bot whose last cycle left repair outstanding (unresolvedGapCount > 0, or candleCount < rawKeepCount, including an unknown count next to a known target) — runs the full cycle instead. Per-bot verdicts are never aggregated, so one lagging bot triggers a catch-up for everyone, and the veto is logged with its reason and bot keys:
Startup: running a catch-up cycle now — no_state_row [broken-bot].
Only bots the adapter would actually process are in scope, so a removed bot's leftover state row cannot keep the daemon awake. kibanaBackfillCount/kibanaGapRepairCount are deliberately ignored: they are action counts from a cycle that already applied its repair, not owed work.
Two deliberate trade-offs:
- An active bot that can never write state (unresolvable market, persistent pre-persist failure) disables the startup sleep for the whole daemon indefinitely — fail-safe by design, since healthy bots still take the per-bot skip; fixing or deactivating the bot restores the sleep.
- A config change made after the last cycle (e.g. a grown AMA window) waits for the next boundary. That backfill is bounded by one poll period, and no candle can close while the adapter sleeps, so no market value is ever late.
One-shot entry points (--once, and runOnceForAma behind ama_signal_runner) always run a full cycle — they exist to produce a computed AMA on demand, not a state-only "skipped" record with null prices.
Skipped work is visible in state: skipped bots record lastCycleSource: "off-hour-skip" with a fresh lastCycleAt, so monitoring can tell an idle-by-design adapter from a dead process. A skip record is a last-known snapshot, not an epoch reset: amaPrice, amaCenterPrice, lastCandleTs and the other data fields keep the values from the last full cycle, and only lastCycleSource/lastCycleAt/pendingClosedCandle/lastTriggerSuppressedReason are refreshed. Between-cycle consumers therefore read the most recent computed values rather than a hole.
Live Writes and Dry-Run
The whitelist controls what the adapter may write. Non-whitelisted bots are still processed, but live grid files and recalc triggers are suppressed.
| Invocation | Behavior |
|---|---|
node dist/market_adapter/market_adapter.js |
Whitelisted bots write live files; others dry-run |
node dist/market_adapter/market_adapter.js --dryRun |
All bots dry-run |
node dist/market_adapter/market_adapter.js --whitelist-all |
All AMA bots write live files |
Dry-run log lines include [DRY RUN] or [suppressed, dry-run].
Useful Commands
| Task | Command |
|---|---|
| Enable/inspect Price, Weight, Range for a bot | dexbot bot → 2) Modify bot → 6) Adapter |
| Remove a deleted bot's entry | Automatic — dexbot bot → 3) Delete bot also drops its whitelist key |
| Probe public CEX availability | node dist/market_adapter/inputs/fetch_cex_synthetic_data.js --exchange auto --check-only |
| Seed synthetic cross candles | node dist/market_adapter/inputs/fetch_cex_synthetic_data.js --exchange auto --bot-key <bot-key> |
| Run one adapter cycle | node dist/market_adapter/market_adapter.js --once |
| Run one cycle with threshold override | node dist/market_adapter/market_adapter.js --once --deltaPercent 1.5 |
| Run continuously | node dist/market_adapter/market_adapter.js |
| Print one-cycle JSON signals | node dist/market_adapter/ama_signal_runner.js |
| Print one bot's compact signal output | node dist/market_adapter/ama_signal_runner.js --bot <botKey> --compact |
--deltaPercent changes the threshold only for that run.
The public CEX synthetic importer uses only the exchanges that passed the live depth test for the requested cross. It ranks them by the historical depth their public candle APIs return for the probe window, filters out sources that do not meet the market-adapter seed requirement, and then seeds the file with a synthetic cross built from two public USDT legs on the best usable exchange. Some exchanges expose alternate market names for the same underlying asset; the importer normalizes those aliases automatically.
The seed depth is calculated from the effective AMA configuration when a bot identity is provided, so the generated file has enough candles for that runtime configuration. The adapter only reads the exact market_adapter_<botKey>_<interval>.json file, so the generated seed must match the bot's eventual botKey.
Troubleshooting
Bot is not processed
- Confirm
gridPriceisama,ama1,ama2,ama3, orama4. - Open
dexbot bot→2) Modify bot→6) Adapterand confirm Price istrue. - Confirm the expected
botKeyexists inprofiles/market_adapter_whitelist.json. - If
startPriceis numeric, the adapter will not fetch pool/book candles for that bot. UsestartPriceonly for a fixed anchor in that case;gridPriceremains a separate grid setting.
Trigger is not created
- Check
lastDeltaPercentvsthresholdPercent. - Check
staleDataandstaleAgeHours. - Confirm the bot is whitelisted. Non-whitelisted bots only log and do not write triggers.
- Confirm the bot's whitelist entry has
"ama": true(dexbot bot→2) Modify bot→6) Adaptershows it as Price). - Run
node dist/market_adapter/market_adapter.js --once --deltaPercent <lower-value>for a one-cycle threshold test.
Trigger fires too often
- Increase
MARKET_ADAPTER.AMA_DELTA_THRESHOLD_PERCENT. - Inspect
lastDeltaPercentto see the normal movement range for the pair.
Adapter will not start
- Check for an old lock file at
market_adapter/state/market_adapter.lock. - If the adapter is not running and the lock is from a crashed process, remove the stale lock file manually.
- If you are using the direct bot launcher, make sure at least one active bot has
gridPriceset toama,ama1,ama2,ama3, orama4. - If you are using PM2, confirm that
dexbot-adapterexists in the PM2 app list.
Related Tools
Export candles and charts:
node dist/market_adapter/inputs/fetch_lp_data.js --pool <poolId> --precA <precA> --precB <precB> --interval 1h --lookback 8760h
node dist/market_adapter/inputs/fetch_lp_data.js --pool <poolId> --precA <precA> --precB <precB> --interval 1h --start <start-date> --end <end-date>
npm run lp:chart -- --data market_adapter/data/lp/<pair>/lp_pool_<id>_<interval>.jsonResearch and calibration:
node dist/analysis/analyze_dynamic_weight.js --file market_adapter/data/lp/<pair>/lp_pool_<id>_<interval>.json
node dist/analysis/analyze_volatility.js --file market_adapter/data/lp/<pair>/lp_pool_<id>_<interval>.json
node dist/analysis/ama_fitting/calibrate_convergence_er.js --data market_adapter/data/lp/<pair>/lp_pool_<id>_<interval>.jsonMore tools:
dexbot tv— one-step candle fetch + chart, with the grid range and AMA recentering overlaid (handy for tuningminPrice/maxPrice)- Analysis
- Scripts
Technical Reference
This section keeps the full market adapter details in one place. Normal operation should follow the Quick Start above.
Purpose and Boundaries
The market adapter bridges historical analysis and live bot operation. It keeps fresh market candles per bot, computes the AMA-based market center, evaluates trend and volatility signals, writes dynamic weight snapshots, and emits recalc triggers when the grid should be rebuilt.
For the similar bot-scoped debt workflow and collateral advisory path, see MPA and Credit Usage.
The adapter runs independently from dexbot.ts. It does not place orders, manage bot lifecycle, or edit bot configuration. Order execution and grid rebuilds stay owned by the bot runtime.
Standalone daemon mode is still available for direct inspection or manual operation:
node dist/market_adapter/market_adapter.jsThe adapter acts only on closed 1h candles. It can poll more often, but live updates wait for the next completed candle.
Signal Pipeline
price_candles -> market_adapter -> AMA -> grid center
AMA slope -> grid range scaling
AMA slope -> directional trend channel
Kalman signal -> trend confirmation channel
ATR -> symmetric volatility penalty
trend + ATR -> dynamic buy/sell weights
trend regime -> advisory collateral-ratio hint
first AMA center -> recalculate.<botKey>.trigger for bootstrap
AMA delta -> recalculate.<botKey>.trigger
AMA slope delta -> recalculate.<botKey>.trigger for grid range scaling bots
Per cycle, per processed bot, the adapter can produce:
| Output | Meaning |
|---|---|
gridCenterPrice |
AMA-derived grid center, clamped to bounds |
weights |
Dynamic { buy, sell } grid weights |
trend |
UP, DOWN, or NEUTRAL |
atr / weightVariance |
Volatility diagnostics |
dynamicWeights |
Runtime payload with effective weights and range-scaling diagnostics |
collateralRecommendation |
Advisory collateral-ratio hint |
recalculate.<botKey>.trigger |
Runtime signal for grid rebuild |
Runtime Flow
- Load active bots from
profiles/bots.json. - Select bots with
gridPriceset toama,ama1,ama2,ama3, orama4. - Resolve AMA settings from
profiles/market_profiles.json. - Read
startPriceto choose the candle source:pool,book, or fixed. - Sync candle data from Kibana or native BitShares data, using the selected source.
- Repair missing candle gaps: auto-fill gaps ≤24 candles directly (no Kibana), query Kibana only for gaps beyond that threshold. Kibana returning empty is treated as verified no-trade. Writes are suppressed while gaps remain unresolved.
- Ignore still-forming 1h candles.
- Compute AMA center, trend, ATR, weights, and collateral hint.
- Persist the first accepted center, compare center delta, and compare whitelisted range-scaling slope delta.
- Suppress live writes if the bot is not whitelisted or candle data is stale.
- Write
dynamicgrid.jsonand a recalc trigger for bootstrap, AMA-center threshold, or grid-range-scaling AMA-slope threshold events. - Persist state snapshots under
market_adapter/state/.
Files
Config and state files (click to expand)
| File | Purpose |
|---|---|
profiles/bots.json |
Active bots, symbols, pool IDs, and gridPrice settings |
profiles/market_profiles.json |
Pair AMA profiles and defaults |
profiles/market_adapter_whitelist.json |
Per-bot live-write permissions |
profiles/market_adapter_settings.json |
Advanced adapter and dynamic-weight tuning |
profiles/general.settings.json |
Global market adapter threshold settings |
market_adapter/state/market_adapter_state.json |
Full runtime state and diagnostics |
market_adapter/state/market_adapter_centers.json |
Lightweight center-price snapshot |
profiles/logs/market_adapter.log |
Standalone adapter runtime log |
What the Adapter Writes
During normal operation, the adapter may write:
| File | Purpose |
|---|---|
profiles/orders/<botKey>.dynamicgrid.json |
Persisted AMA center snapshot, AMA slope diagnostics, and optional dynamic weights used by the bot runtime |
profiles/recalculate.<botKey>.trigger |
Signal for dexbot.ts to rebuild the grid |
market_adapter/state/market_adapter_state.json |
Full runtime state and diagnostics |
market_adapter/state/market_adapter_centers.json |
Lightweight center-price snapshot |
market_adapter/data/ |
Candle caches and exported LP data |
profiles/logs/market_adapter.log |
Standalone adapter runtime log |
dexbot.ts consumes the recalc trigger and handles the grid rebuild. The market adapter does not place orders, start bots, stop bots, or edit profiles/bots.json.
Dynamic Grid Snapshot
profiles/orders/<botKey>.dynamicgrid.json is the live snapshot that the bot runtime reloads on selected rebalance and maintenance paths. It is written with the current AMA-derived center and, when enabled, the live dynamic-weight payload.
Typical fields:
{
"gridCenterPrice": 1294.6,
"centerPrice": 1294.6,
"amaCenterPrice": 1294.6,
"amaSlope": {
"trend": "UP",
"slopePct": 0.04,
"slopeOffset": 0.16
},
"gridPriceOffsetPct": 0.8,
"amaSlopeDeltaPercent": 0.015,
"amaSlopeThresholdPercent": 0.015,
"updatedAt": "2026-03-01T00:00:00.000Z",
"source": "market_adapter/market_adapter.ts",
"dynamicWeights": {
"effectiveWeights": { "sell": 0.45, "buy": 0.55 },
"baseWeights": { "sell": 0.5, "buy": 0.5 },
"isReady": true
}
}gridCenterPriceis the persisted grid baseline used for future delta checks.centerPriceremains as a compatibility alias for older readers.amaCenterPriceis the raw AMA output before downstream handling.amaSlopeis the latest AMA slope snapshot used for diagnostics and snapshot writes.gridPriceOffsetPctis the signed market/start-price offset derived from AMA slope and capped at half oftargetSpreadPercent; it is not applied togridCenterPrice.gridRangeScalingAmaSlopein adapter state is the last grid-reset slope baseline used for slope-triggered range-scaling resets.amaSlopeDeltaPercentrecords the change from the last grid-reset slope baseline.amaSlopeThresholdPercentis the configured slope-reset threshold.amaSlope.trendrecords the current direction; direction changes do not trigger resets unless the slope delta threshold is crossed.- During a full grid reset, the bot refreshes
gridCenterPricefrom the latestamaCenterPricebefore rebuilding the grid. dynamicWeightsis present only when live dynamic weights were computed and the bot is allowed to consume them.- The runtime applies
dynamicWeightsonly when the bot is whitelisted fordynamicWeightand the snapshot reportsisReady: true. - The bot reads this snapshot before fill processing and other selected structural maintenance so new orders use the latest accepted center and weights.
Module Map
Directory layout (click to expand)
market_adapter/
|-- market_adapter.ts main adapter daemon
|-- ama_signal_runner.ts one-cycle JSON signal CLI
|-- candle_utils.ts candle transforms, gap detection, pruning
|-- interval_utils.ts shared interval label helpers
|-- lp_chart_core.ts chart HTML renderer
|-- lp_chart_strategy_loader.ts AMA strategy/profile resolver for charts
|-- lp_chart_runner.ts LP chart orchestration
|-- log_format.ts adapter startup and signal log formatting
|-- test_helpers.ts test utilities
|-- core/
| |-- asymmetric_bounds.ts AMA-slope range scaling helpers
| |-- market_adapter_service.ts full signal pipeline service
| |-- config_normalizers.ts shared config normalization
| |-- kibana_client.ts low-level Kibana/ES query client
| |-- kibana_candles.ts LP pool candle fetch engine
| |-- kibana_market_candles.ts book candle fetch and transform
| |-- signals/
| | |-- hurst_analyzer.ts Hurst Exponent analysis
| | |-- kalman_trend_analyzer.ts Kalman trend state tracking
| | |-- kalman_velocity_smoothing.ts Adaptive Kalman velocity smoothing
| | `-- permutation_entropy_analyzer.ts Permutation Entropy analysis
| `-- strategies/
| |-- ama.ts Kaufman's Adaptive Moving Average (KAMA)
| |-- ama_slope_model.ts AMA slope and trend weight logic
| |-- collateral_manager.ts advisory collateral-ratio logic
| |-- dynamic_weight_series.ts canonical per-bar AMA/Kalman offset pipeline
| |-- regime_gate.ts regime multiplier gating
| |-- regime_interp.ts regime table bilinear interpolation
| |-- volatility_shift.ts symmetric ATR volatility penalty
| `-- atr/calculator.ts ATR calculation
|-- inputs/
| |-- kibana_source.ts Elasticsearch LP data source
| |-- fetch_lp_data.ts historical LP candle exporter
| |-- fetch_book_data.ts order-book candle exporter
| |-- kibana_feed_source.ts MPA feed candle source
| |-- window_cache.ts shared month-shard candle cache
| `-- fetch_cex_synthetic_data.ts public CEX synthetic-candle seed importer
|-- utils/
| |-- chain.ts blockchain query helpers
| |-- adapter_client.ts inter-process credential daemon client
| |-- native_history.ts native BitShares market history fetch
| |-- file_lock.ts single-instance file lock
| |-- data_discovery.ts data directory auto-discovery
| |-- atomic_write.ts atomic file write utility
| |-- dynamic_grid_snapshot.ts dynamic grid snapshot helpers
|-- data/ runtime candle caches and exports
`-- state/ runtime state, centers, and lock file
Whitelist Semantics
profiles/market_adapter_whitelist.json controls live writes. It is edited per bot in the bot editor (dexbot bot → 2) Modify bot → 6) Adapter), which reads and writes this same file:
{
"whitelist": {
"<botKey>": {
"ama": true,
"dynamicWeight": false,
"asymmetricBounds": false
}
}
}ama: trueallows livedynamicgrid.jsonand recalc trigger writes.dynamicWeight: trueallows dynamic weights to be applied by the bot runtime, but only when the snapshot is also marked ready.asymmetricBounds: trueallows AMA-slope grid range scaling during grid rebuilds. Runtime code reports this asgridRangeScalingWhitelisted; the whitelist key remainsasymmetricBoundsfor compatibility.- Omitted whitelist flags are treated as
false. - Missing whitelist file means all live AMA writes are suppressed.
- Missing bot entry means that bot runs in dry-run mode.
Grid Range Scaling Model
Grid range scaling is the technical path for:
AMA slope -> min/max bound tilt
AMA slope -> live market/start-price offset, capped at half spread
During a grid rebuild, the bot loads the latest dynamic grid snapshot and uses the AMA slope diagnostics to tilt the configured minPrice and maxPrice around the AMA center. A reciprocal (log-symmetric) tilt scales both bounds by the same factor: an uptrend shifts the whole band up by 1 + asymmetry, a downtrend shifts it down by 1 / (1 + asymmetry). The trend side therefore extends while the opposite side tightens toward the center, while total log width (and slot count) is preserved.
The same asymmetricBounds whitelist also enables gridPriceOffsetPct: a slope-ratio offset applied only to the live startPrice used for initial placement. It is capped at half of targetSpreadPercent (2 allows -1% to +1%) and does not change gridCenterPrice, the AMA center, or reset threshold baselines.
This uses dynamicWeights.trend, dynamicWeights.slopeOffset, and dynamicWeights.maxSlopeOffset, but it is separate from the dynamic buy/sell weight shift. The whitelist flag is asymmetricBounds.
slope = Huber-robust regression of ln(AMA) over the lookback window, in percent per bar
slopeOffset = slope normalized to the configured dynamic-weight slope cap
asymmetry = min(|slopeOffset| / maxSlopeOffset, 1) × maxAsymmetryFactor
Downtrend: minPrice = center / (M × (1 + asymmetry))
maxPrice = (center × M) / (1 + asymmetry)
Uptrend: maxPrice = center × (M × (1 + asymmetry))
minPrice = (center / M) × (1 + asymmetry)
Neutral: symmetric bounds (asymmetry = 0)
ASYMMETRIC_BOUNDS_MAX_ASYMMETRY_FACTOR defaults to 0.333; 0 disables the tilt. ASYMMETRIC_BOUNDS_MIN_SCALE_SLOTS defaults to 10 and sets the minimum number of price levels the tightened side of a range-scaled grid must keep between the grid center and its bound (measured in incrementPercent steps); 0 disables the guard. Together they keep the widened side aggressively extended while guaranteeing the narrowed side never collapses into a near-center sliver with few or no active orders.
Both are configurable per bot or per market via profiles/market_adapter_settings.json (see the layering in Per-Bot Overrides):
{
"globals": {
"asymmetricBounds": {
"maxAsymmetryFactor": 0.333,
"minScaleSlots": 10
}
},
"pairs": [{
"assetASymbol": "IOB.XRP",
"assetBSymbol": "BTS",
"marketAdapterSettings": {
"asymmetricBounds": { "minScaleSlots": 3 }
}
}]
}asymmetricBounds is merged per-field across the globals → pairs[].marketAdapterSettings → pairs[].botOverrides.<bot> layers, so overriding one field (e.g. a market-level minScaleSlots) does not wipe another (e.g. a bot-level maxAsymmetryFactor).
Trigger Files
When the threshold is exceeded, the adapter writes a trigger file under profiles/ at the repo root:
profiles/recalculate.<botKey>.trigger
The file contains a trigger payload like:
{
"createdAt": "2026-03-01T00:00:00.000Z",
"source": "market_adapter/market_adapter.ts",
"botName": "<botName>",
"botKey": "<botKey>",
"thresholdPercent": 0.8,
"deltaPercent": 1.1,
"previousCenterPrice": 1280.5,
"newCenterPrice": 1348.32,
"referencePrice": 1294.6,
"amaCenterPrice": 1294.6,
"poolId": "<poolId>"
}dexbot.ts watches for this file and rebuilds the affected grid from current runtime state. The trigger is separate from dynamicgrid.json: the trigger requests a rebuild, while the snapshot carries the center and live weight state that the runtime can reload.
Dynamic Weight Model
Dynamic weights start from the bot's configured weightDistribution. These configured values are the static baseline.
The production dynamic-weight output combines:
| Branch | Role |
|---|---|
| AMA slope | Measures filtered market direction and velocity |
| Kalman signal | Confirms directional movement |
| ATR volatility | Applies a symmetric risk penalty to both sides |
| Regime gates | Suppress weak or noisy signals |
On each closed-candle cycle, the adapter applies two adjustments:
| Adjustment | Signal | Formula effect |
|---|---|---|
trendOffset |
AMA slope + Kalman confirmation | Subtracts from sell, adds to buy |
volatilityPenalty |
Volatility | Adds the same normally negative value to both sides |
effectiveSell = staticSell - trendOffset + volatilityPenalty
effectiveBuy = staticBuy + trendOffset + volatilityPenalty
A positive trendOffset shifts weight toward buy and away from sell. A negative trendOffset shifts weight toward sell and away from buy. The final values are clamped and rounded before being written to the dynamic grid snapshot.
Main override knobs live in profiles/market_adapter_settings.json:
Full settings table (click to expand)
| Setting | Meaning |
|---|---|
alpha |
AMA vs Kalman blend |
dw |
Kalman displacement weighting |
gain |
Output amplitude |
amaSlopePercentMode |
Slope override units: perBar for percent per bar, or window/unset for legacy cumulative percent over the lookback |
amaSlope.lookbackBars |
AMA slope lookback; slope is averaged per bar over this window (default 16) |
amaSlope.persistBars |
Slope-delta persistence gate: consecutive confirming bars required before the slope reset fires (default 3; 1 = legacy fire-on-first-crossing). Values < 1 fall through to the global default, so use 1 (or persistEnabled: false) to disable, not 0. Global default in MARKET_ADAPTER.AMA_SLOPE_PERSIST_* |
amaSlope.neutralZonePct |
Dead band around flat average AMA slope |
amaSlope.maxSlopePct |
Average AMA slope saturation |
amaSlopeDeltaThresholdPercent |
Average AMA slope delta threshold for slope-based resets |
minOutputThreshold |
Minimum trend output before directional shift applies |
maxSlopeOffset |
Cap for asymmetric trend offset |
maxVolatilityOffset |
Cap for symmetric ATR penalty |
clipPercentile |
Outlier filter for AMA/Kalman velocity (clips top N% of values) |
absoluteThreshold |
Dead band before regime filtering |
atrPeriod |
ATR lookback |
volatilityExponent |
ATR penalty exponent |
volatilityScaleX |
ATR penalty scale |
volatilityThreshold |
Minimum volatility penalty before applying shift |
kalmanSmoothPct |
Raw vs smoothed Kalman blend |
dispScaleMinPct |
Kalman displacement minimum scale floor |
kalmanDispScaleMult |
Kalman displacement scale multiplier |
kalmanDispThresholdMult |
Kalman displacement threshold multiplier |
kalmanSlope.maxSlopePct |
Kalman slope saturation |
kalmanSmoothSpanPct |
Adaptive EMA span ratio |
signalConfirmBars |
Signal latch confirmation bars |
hurstZoneBand |
Hurst neutral-zone width for regime classification |
peNodes |
Permutation Entropy thresholds for regime classification |
regimeTable |
Custom 3×3 regime multiplier table |
kibanaRequestTimeoutMs |
Kibana request timeout in milliseconds |
staleTailThreshold |
Stale-tail pruning threshold in candles |
maxNativeGapFillCandles |
Max missing-candle gaps auto-filled without a Kibana query |
When migrating older settings, either divide AMA slope percent overrides by amaSlope.lookbackBars, or add "amaSlopePercentMode": "window" and let the adapter convert them at load time. New settings should use "amaSlopePercentMode": "perBar" so small per-bar values are not converted again by pair or bot overrides.
Most operators should tune only the price and slope trigger thresholds plus the AMA profile unless they are deliberately fitting a market.
See also research guide.
Candle and Staleness Handling
The adapter keeps candle caches current using Kibana bootstrap plus native incremental updates. Missing candle gaps are repaired in two steps: (1) auto-fill gaps ≤24 candles (trusted threshold) by carrying the preceding close forward with zero volume — Kibana is redundant since native fetch already confirmed no trades; (2) Kibana query for gaps >24 candles — real candles are merged in, or an empty response is treated as verified no-trade (all gaps in the queried window are synthesized). Gaps remaining after both steps suppress writes via unresolved_candle_gaps until repaired on a future cycle. The adapter prunes old candles to the required AMA window and acts only on closed 1h candles.
Shared Month-Shard Cache and Fetch Robustness
Pool, book, and feed candle fetches share one cache entry point (runCachedWindows in market_adapter/inputs/window_cache.ts): candles live in fixed calendar-month shards (<base>.shard_YYYY-MM.json, UTC) whose names never shift, so a run loads only the shards overlapping its requested range, queries only genuinely missing buckets plus a bounded tail refresh, and rewrites only shards that gained buckets or query coverage — pure-reuse runs perform zero writes and zero deletes. Shard metas record the ranges actually queried (meta.queriedRanges, monotonically unioned) together with at, the time each query ran; spans merge only when they assert the same verification time, so per-bucket freshness stays exact. A missing range is pruned only when recorded query coverage genuinely covers it — the absence of local buckets alone never certifies history as empty — and only once the range ended more than GAP_SETTLE_HORIZON_MS ago, judged per gap rather than per window (a month-old gap at the leading edge of the newest window is as settled as one in a fully past window). The tail window re-queries only buckets that no query has seen TAIL_SETTLE_LAG_MS after they closed (settleCoverage), so a rerun costs ~lag+elapsed hours instead of a fixed 48h, and the refresh never reaches back past TAIL_REFRESH_HOURS (an unsettled bucket must be refreshed because index lag is still open, but a coverage hole older than 48h is past any plausible lag and must not drag the boundary back weeks); caches without at fall back to that fixed window. Clusters of gaps within GAP_MERGE_TOLERANCE_BUCKETS collapse into one query, and the sub-range path is bounded by merged span hours rather than gap count, so one extra small hole can no longer escalate a run to a full-window fetch. Because spans merge only on an identical at, a shard gains one span per re-verifying run; the persisted list is capped at MAX_COVERAGE_SPANS by dropping the oldest spans, which can never invent emptiness (it can only make those buckets look unsettled, bounded by the same 48h ceiling). A run that re-verifies the tail therefore rewrites that shard even when the candles come back identical — the moved verification time is the news, and skipping the write would throw it away; pure-reuse runs still write nothing. Partial windows merge into the run output but are never persisted. Stable month shards are the only supported cache format; obsolete run-relative cache files are ignored. Every range fetch runs through fetchRangeWithRetry (per-range attempts + linear backoff + abort-signal timeout; the LP path keeps a 4-attempt budget), one-shot Kibana queries retry transient errors (3 attempts), paged fetchers cap at kibanaMaxPages (500), and bidirectional fetches tolerate a one-direction failure.
AMA Warmup Window — Why Candle Length Matters
The AMA is a recursive (infinite impulse response) filter. On cold start, the adapter uses an initial warmup phase: it calculates an SMA (Simple Moving Average) over the first erPeriod candles to establish a stable seed price, while simultaneously building the price history needed to calculate the first valid Efficiency Ratio (ER).
Starting the recursive AMA formula from this SMA, rather than a single raw closing price, provides a more stable anchor. However, a residual initialization bias still exists and decays asymptotically — each bar, the AMA "forgets" a fraction equal to its smoothing constant:
bias_remaining(K) ≈ bias_initial × ∏ (1 − SC_i) for i = 1..K
Kaufman's smoothing constant is the ER-scaled value, squared:
SC_i = [ER_i × (fastSC − slowSC) + slowSC]²
where fastSC = 2 / (fastPeriod + 1)
slowSC = 2 / (slowPeriod + 1)
Because ER_i varies bar-by-bar, a typical-market ER (ER_avg) is used to estimate an average decay rate:
SC_avg = [ER_avg × (fastSC − slowSC) + slowSC]²
Bars needed to reduce bias below a target fraction ε:
convergenceBars = ln(ε) / ln(1 − SC_avg)
The adapter keeps the full warmup window in candle history so the AMA seed and convergence bias are retained for downstream calculations:
amaWarmupBars = erPeriod + convergenceBars + lookbackBars
| Component | Role |
|---|---|
erPeriod |
Bars for the first Efficiency Ratio value to become available |
convergenceBars |
Bars to decay 99 % of the cold-start initialisation bias |
lookbackBars |
Extra lookback for slope/trend analysis (AMA slope, ATR) |
For AMA slope readiness and percentile clipping, the earlier gate is:
amaSlopeReadyBars = erPeriod + lookbackBars
That threshold is enough once the ER window exists and the lookback comparison bar is available. The longer amaWarmupBars window is still retained so the underlying AMA series has its full convergence history.
The two calibration constants live in modules/constants.ts under MARKET_ADAPTER:
| Constant | Value | Meaning |
|---|---|---|
AMA_CONVERGENCE_ER_AVG |
0.151 |
Typical-market Efficiency Ratio. Lower = more conservative (assumes more noise, slower convergence, more candles needed). Calibrated against the fetched 3-year pool 133 1h dataset (2023-05-07 -> 2026-05-06); corrects for Jensen's inequality — E[f(ER)] ≠ f(E[ER]) when f is the squaring function. |
AMA_CONVERGENCE_EPSILON |
0.01 |
Target remaining bias fraction. 0.01 means 99 % of the initial bias has decayed by the end of the convergence window. |
STALE_TAIL_THRESHOLD_CANDLES |
24 |
Trusted no-trade gap threshold. Gaps ≤24 candles are auto-filled without Kibana; gaps beyond query Kibana. Also used as stale-tail pruning threshold. |
To recalibrate AMA_CONVERGENCE_ER_AVG against new market data, use the research script:
node dist/analysis/ama_fitting/calibrate_convergence_er.js [--data <lp-file.json>] [--amas AMA3,AMA4]See analysis/ama_fitting/calibrate_convergence_er.ts for details on the implied-ER correction (Jensen's inequality).
For a calibrated AMA_CONVERGENCE_ER_AVG of 0.151 and AMA_CONVERGENCE_EPSILON = 0.01 in 1-hour candles, the full AMA warm-up for the built-in presets is:
| Preset | Candles | Days |
|---|---|---|
AMA1 |
1,602 |
66.8 |
AMA2 |
1,677 |
69.9 |
AMA3 |
1,764 |
73.5 |
AMA4 |
1,844 |
76.8 |
These totals include the ER buffer, the convergence window, and the default 9-bar lookback used by the dynamic-weight logic. If the candle timeframe is not 1 hour, scale the total by the candle duration.
How slowPeriod affects the warm-up: the estimate blends the smoothing constant at a typical ER: SC_avg = (ER_avg · fastSC + (1 − ER_avg) · slowSC)² with slowSC ≈ 2 / slowPeriod (see getAmaWarmupBars in market_adapter/core/strategies/ama.ts). Since convergenceBars is proportional to 1 / SC_avg, a larger slowPeriod extends the warm-up, but the ER_avg · fastSC term bounds SC_avg from below, so at the calibrated average ER the growth is sub-quadratic: doubling slowPeriod (84 → 168) raises the requirement only ~1.4×. The O(slowPeriod²) scaling (doubling quadruples it) applies only in the pure-choppy limit (ER → 0), where SC = slowSC². For the AMA3 default (see MARKET_ADAPTER.AMAS in modules/constants.ts) the blended estimate is ~974 convergence bars.
If the adapter has fewer candles than the warmup window, the AMA output is too biased for grid centering and the cycle skips with reason ama_warmup_insufficient.
Stale data suppresses trigger writes. Check staleData and staleAgeHours in market_adapter/state/market_adapter_state.json when a trigger should have fired but did not.
State Files
| File | Contents |
|---|---|
market_adapter/state/market_adapter_state.json |
Full per-bot state, signals, weights, staleness, and diagnostics |
market_adapter/state/market_adapter_centers.json |
Compact center-price snapshot |
market_adapter/state/market_adapter.lock |
Single-instance runtime lock |
If the adapter crashed and is no longer running, a stale lock file can be removed manually.
Monitoring Fields
Important fields in market_adapter/state/market_adapter_state.json:
Field reference (click to expand)
| Field | Meaning |
|---|---|
meta.updatedAt |
Last completed adapter cycle (ISO timestamp) |
lastCycleSource |
How the last cycle ended the bot: a fetch source (native-incremental-overlap, kibana-backfill, …) or off-hour-skip when the closed-candle gate skipped the work |
meta.metrics.processedBots |
Number of bots evaluated this cycle |
meta.metrics.durationMs |
Cycle wall-clock duration in milliseconds |
lastCycleAt |
Last cycle timestamp for a bot |
lastAmaPrice |
Latest computed AMA price for a bot |
gridCenterPrice |
Stored center used for delta comparison |
lastDeltaPercent |
Move from stored center to latest AMA |
thresholdPercent |
Active recalc trigger threshold |
lastTriggerFile |
Last recalc trigger file written (triggered is a transient per-cycle return value, not persisted) |
lastTriggerSuppressedReason |
Why the last trigger write was suppressed |
triggerCount |
Number of triggers written for the bot |
staleData |
Whether stale candles suppressed live writes |
staleAgeHours |
Age of the newest usable candle |
amaSlope.trend |
UP, DOWN, or NEUTRAL |
atr |
Average True Range value |
weightVariance |
Normalized volatility ratio |
weights |
Current dynamic buy/sell weights |
collateralRecommendation |
Advisory collateral-ratio hint |
kibanaGapRepairCount |
Gaps patched this cycle (auto-fill or Kibana-verified) |
unresolvedGapCount |
Gaps still missing after all repair attempts; writes suppressed while > 0 |