ASkewMM_VWAP — Status & roadmap
Where this strategy is today
ENGINEERING COMPLETE — the code is correct, uniform and shippable. That is a statement about engineering, not about profitability; the performance verdict is separate and sits below it.
| Dimension | State |
|---|---|
| Correctness | 5 / 5 invariants pass |
| Console output | 21 tagged calls, 0 off-format |
| Side panel | 6 rows, padded to the 3-column grid |
| Chart marks | drawn only after the exchange confirms a fill |
| Palette | 8 / 8 canonical |
| GUI settings | 16 exposed · 14 read at runtime · 0 untunable · 2 inert |
| Deployed to a live install | Not yet — audited and staged, not yet shipped |
Performance verdict. Over three years on 7 markets it returns a median -0.9% against buy-and-hold's 49.6% — an edge of -68.8% — and is profitable on 3 of 7. On this window it is not a replacement for simply holding the asset, and nothing in these reports claims it is.
What it actually does
A two-sided quoting market maker from the pack's shared neutral-mm template, with an Avellaneda-Stoikov inventory skew. Thirty-three other strategies in the pack are this same engine with different settings.
Each cycle it computes a reference, offsets it against the inventory it is carrying, and quotes a price either side:
- Reference —
N_refselects one of sma / ema / donmid / kama / vwap atN_refPeriod. This file ships ema(382). - Reservation price — the reference shifted against inventory:
ref − skew × (invFrac − target) × band, withN_skew3.3234 andN_target29.34%. Carrying more than the target pushes both quotes down, which is what makes it self-rebalancing in principle. - Half-spread —
ATR(50) × 4.3533, now floored at the net per-side cost (see the fixes report). - Quotes — buy at
reservation − halfSpread, sell atreservation + halfSpread. A fill is taken when the candle's low reaches the buy quote or its high reaches the sell quote. - Clip — a fixed size,
N_clipPct18.18% of starting capital, computed once on the first cycle and never resized. - Gates — quoting pauses entirely above
N_rangeAdx(ADX 29.42), and a trend guard blocks buys when price is below a falling SMA(281). - One action per candle. Each cycle places at most one order and returns.
It is called neutral and it is 99% long
The strategy declares an inventory band: N_invMin 20.69% and N_invMax 50.57%, around a 29.34% target. Measured over 3 years and 111,044 bars of 15m data, peak inventory exceeded the declared ceiling on all seven markets:
| ADA | BTC | DOGE | ETH | LINK | SOL | XRP | |
|---|---|---|---|---|---|---|---|
| peak inventory | 83% | 95% | 88% | 78% | 84% | 98% | 99% |
| mean inventory | 34% | 42% | 38% | 31% | 34% | 51% | 36% |
Against a declared ceiling of 50.57%. The mean on SOL is already above it.
The ceiling is a buy filter, not a bound. It appears once, in the buy gate — invFrac < N_invMax/100 — and nowhere else. Two things then defeat it. A clip is 18% of starting capital, so a buy taken at 49% inventory lands above the ceiling in one step and nothing asks it to come back. And invFrac is a value fraction, so a rally lifts it without a single order being placed: hold 50% and let the asset double, and you are at 67% having done nothing. There is no path anywhere in the file that sells because inventory is too high — the sell fires only when the price reaches the sell quote, one clip at a time, at most once per candle.
So the inventory control that the name, the header and the GUI all describe is real on the way in and absent on the way out. The consequence is in the drawdown column: a median of 57.1%, which is not a market maker's risk profile, it is a long position's.
What the result is made of
Median return −0.87% over three years against a buy-and-hold of +49.6%. Median profit factor is 0.98 — a hair under break-even per unit of risk — across roughly 120 round trips per market, with a median win rate of 53%.
Median fee drag is 7.7% of the account. That is real but it is not the story; a profit factor of 0.98 says the trades themselves are close to a coin flip, and the 57% drawdown says the inventory is where the risk lives. This is a strategy whose trading is roughly break-even and whose outcome is therefore decided by what its unbounded inventory happened to do.
The two profitable markets, BTC (+8.9%, PF 1.08) and SOL (+33.8%, PF 1.15), are the two where buy-and-hold returned +161% and +308%. It was long, in a rally, by accident rather than by design.
The average quote fill loses money
The repair to the sell log (see the fixes report) made a number visible that had never been recorded anywhere. Across three years and seven markets the strategy places 865 resting sells, and the ladder now accounts for every one:
| exits classified | 865 of 865 (was 1 of 186) |
| mean realised gain per fill | −0.19% |
| summed across all fills | −165% |
| worst single fill | −33.11% |
865 fills at −0.19% each is the −165% that the seven market returns add up to. The average resting sell fills below what the inventory it is selling cost — which is the definition of being adversely selected, and it is the entire result.
That sits directly on top of the inventory finding above. The sell quote is built from a reference that follows price, so in a falling market the strategy keeps buying (inventory rises, unbounded) while its sell quote follows the reference down — and fills below the average cost of everything bought on the way. A bounded inventory would cap how far that can go; there is no bound.
Read from the shipped strategy source, not from its documentation.
The measured constraint
Time in market is 99.2%, over 865 round trips across all 7 markets and three years. Everything in the performance report rests on that fraction of the bars.
Settings, measured
The GUI exposes 16 settings. Which ones this strategy actually reads was measured by running it with a recording config proxy rather than inferred from the source — a neutral-mm file bakes 16 keys and only the active mode's branch reads any of them.
- Read at the shipped defaults:
N_atrMult,N_atrPeriod,N_clipPct,N_invMax,N_invMin,N_rangeAdx,N_ref,N_refPeriod,N_skew,N_spreadType,N_target,N_trendGuard,N_trendSma - Read only once you change
N_spreadType— the value that first reaches each:N_spreadPctunderanything but atr - Exposed but never read on this code path:
N_fee,N_rebate— turning these does nothing
What to build next
Ordered by expected effect on the result, not by effort.
1. Enforce the inventory ceiling on the way out, not only on the way in
Highest priority
The declared ceiling is 50.57% and measured peak inventory is 78% to 99% on all seven markets. The ceiling is tested once, in the buy gate, and there is no path that reduces inventory because it is too high.
Two separate leaks, and both need closing:
The clip does not respect the headroom. A clip is 18% of starting capital and is taken whole whenever inventory is below the ceiling, so a buy at 49% lands near 67%. Size the buy to min(clip, headroom) where headroom: equity × invMax − heldValue, which is the same repair already applied to the Directional engine elsewhere in this pack.
Nothing sells down. invFrac is a value fraction, so a rally lifts it with no order placed at all — 50% inventory through a doubling is 67% inventory. The engine needs a reduce-to-band path: when invFrac > invMax, quote the sell side more aggressively, or cross the spread for the excess. Today the only sell is a resting quote taken one clip at a time, at most once per candle, which cannot keep up with a fast market.
Measure the drawdown, not the return. A median drawdown of 57.1% is the number this is meant to move.
2. The skew is the engine's one distinctive idea and it has never been isolated
High priority
The reservation price shifts against inventory — ref − skew × (invFrac − target) × band — which is the Avellaneda-Stoikov idea the family is named for and the mechanism that is supposed to make the inventory self-correcting. It ships at N_skew 3.3234, a six-decimal value left behind by an optimiser sweep.
Given that inventory reached 99% on two markets, the skew is either too weak to matter against an 18% clip, or it is being overwhelmed by the value-fraction effect described above. Which of those it is decides whether the fix above is sufficient or whether the skew itself needs re-tuning.
The experiment is cheap and it is a sweep of one setting: run N_skew at 0, 1, 3.3234 and 8 across all seven markets and read peak inventory and drawdown. A skew of 0 turns the reservation into the plain reference and is the control.
3. Resize the clip from current equity, not from starting capital
High priority
clipBase is computed once, on the first cycle, as initialCapital × N_clipPct / 100 divided by price — and never recomputed. It is a fixed number of base units for the life of the run.
That has two consequences. An account that has grown keeps trading at its original size, so the strategy under-deploys; an account that has lost half keeps trading at the old size, so every clip is a larger share of what remains, which is the wrong direction on both counts. On ADA, which ended −38.1%, the final clips were nearly a third larger relative to the account than the first ones.
The comment says this mirrors the backtest, and matching the reference implementation is a legitimate reason — but it should be a setting rather than a silent assumption, and the drawdown cost of the fixed size ought to be measured against a re-based one.
4. Ask whether the ADX pause is helping
Worth testing
Quoting stops entirely above ADX 29.42, on the reasoning that a maker should not quote into a trend. But the strategy is long 31–51% of its equity on average, so pausing does not flatten it — it stops the selling as well as the buying, and leaves the inventory exposed to exactly the move the gate detected.
That is worth testing directly, because the intuition and the implementation point in opposite directions: a maker with no inventory should pause in a trend, and a maker holding half its book arguably should be doing the opposite. Run the gate on, off, and sell-side-only across the seven markets.
5. The name is wrong and cannot be fixed by renaming
Read before acting on the others
This file is called ASkewMM_VWAP and ships N_ref: "ema". A rolling VWAP anchor is available — refSeries() returns one when N_ref is set to vwap — but it is not what ships.
The header now says so explicitly. The filename is left alone on purpose: it is the key an operator's existing configuration already names, and renaming it would break a working install to correct a comment. Set N_ref to vwap if the name is what you wanted, and be aware you will be running an untested configuration — every number on this page was measured with the EMA anchor.
Seventeen strategies in this family carry a false claim in their filename; see the fixes report for the survey.
Operator output, measured
Everything below was read out of the strategy file by the presentation audit, not taken from its documentation. These are the surfaces an operator actually watches: the terminal log, the side panel, the chart, and the settings the GUI offers.
- Tagged, aligned, coloured
- 21
- Raw and off-format
- 0
- Exempt (object and error dumps)
- 7
Every non-exempt line carries the strategy name, an event tag and the palette colour for that tag, so one terminal running many pairs stays readable.
- Data rows
- 6
- Section headers
- 1
- Rows with no tooltip
- 0
- Padded to the 3-column grid
- yes
- Model
- fill-confirmed
A mark is drawn only once the exchange confirms the fill, so the chart never shows a trade that did not happen.
- Non-canonical colours
- 0
One colour per meaning, identical in the log, the panel and the chart — the eight canonical roles.
Settings the GUI exposes
16 settings appear in the Gunbot strategy editor for this strategy. 14 of them are read at runtime — measured by running the strategy with a recording proxy over its config, not inferred from the source.
N_atrMultN_atrPeriodN_invMaxN_invMinN_rangeAdxN_refN_refPeriodN_skewN_spreadPctN_spreadTypeN_trendSmaN_clipPctN_targetN_trendGuardN_feeN_rebate
Open presentation findings
- DEFECT · inputs — 2 knob(s) exposed in the GUI are never read on this strategy's code path — turning them does nothing: N_fee, N_rebate