Skip to content

Execution algorithms (TWAP / VWAP / Iceberg / POV)

A strategy emits a target intent ("buy 1.0 BTC over the next hour" or "sell into 5 percent of market volume"). The execution algorithm turns that intent into a stream of child orders. flox ships four composable algos. They live in the C++ engine and are exposed through every binding (Python, Node, Codon, QuickJS) with the same surface: step(now_ns) drives the state machine and yields any newly emitted child orders, the user dispatches them through whichever executor is running, and report_fill(qty) (plus observe_volume(qty) for POV) feeds state back so the next step makes the right decision.

When to reach for which algo

Algo Use when
TWAPExecutor You want to spread an order evenly in time. Equal-time slicing across a fixed duration. The default for "I have an hour, slice it into 12 child orders".
VWAPExecutor You have a volume curve (historical or live forecast) and want slice sizes proportional to expected volume. Better than TWAP when intraday volume is uneven.
IcebergExecutor You want to hide size. Show only visible_qty; resubmit the next slice when the previous one fills.
POVExecutor You want to participate at a fixed share of observed market volume. Adapts to live conditions; you feed it observe_volume(qty) from your trade tick.

All four track target_qty, submitted_qty, filled_qty, and remaining_qty. None of them place duplicate orders or exceed the target. None of them know anything specific about your engine; they call submit_order and ask you to feed them fills (and, for POV, observed volume).

Quick start

import flox_py as flox
from flox_py.execution_algos import TWAPExecutor

sim = flox.SimulatedExecutor()

twap = TWAPExecutor(
    target_qty=10.0, side="buy", symbol=1,
    duration_ns=3_600_000_000_000,
    slice_count=12, start_time_ns=now_ns,
)

def on_tick(now_ns):
    twap.step(now_ns, sim)
    if twap.is_done():
        print(twap.submitted_qty, twap.filled_qty)

def on_fill(qty):
    twap.report_fill(qty)
const flox = require('@flox-foundation/flox');

const twap = new flox.TWAPExecutor({
  targetQty: 10, side: 'buy', symbol: 1,
  durationNs: 3_600_000_000_000,
  sliceCount: 12, startTimeNs: nowNs,
});

function onTick(nowNs) {
  const children = twap.step(nowNs);
  for (const c of children) sim.submitOrder(c.orderId, 'buy', c.price, c.qty);
  if (twap.isDone()) console.log(twap.submittedQty(), twap.filledQty());
}

function onFill(qty) { twap.reportFill(qty); }
from flox.execution_algos import TWAPExecutor, SIDE_BUY, TYPE_MARKET

twap = TWAPExecutor(10.0, SIDE_BUY, 1,
                    3_600_000_000_000, 12, 0,
                    TYPE_MARKET, 0.0)
n = twap.step(now_ns)
var twap = new flox.TWAPExecutor({
    targetQty: 10, side: 'buy', symbol: 1,
    durationNs: 3600000000000, sliceCount: 12, startTimeNs: 0,
});
var children = twap.step(nowNs);

The same pattern works for the other three algos. The only differences are the constructor arguments and what each one needs to know to make its decisions.

VWAP volume curve

VWAPExecutor takes volume_curve as a sequence of (bar_ts_ns, volume) pairs ordered by timestamp. Each bar's slice is (bar_volume / total_volume) * target_qty. Bars with zero volume are skipped.

from flox_py.execution_algos import VWAPExecutor

curve = [
    (bar_ts_ns(9, 30), 1_000.0),
    (bar_ts_ns(9, 31), 1_500.0),
    (bar_ts_ns(9, 32), 2_000.0),
    # ... full intraday profile
]
vwap = VWAPExecutor(
    target_qty=100.0, side="buy", symbol=1,
    volume_curve=curve,
)

Use a historical 5-day average curve as a starting point. If you have a live volume forecaster, feed its output into the curve; the algo recomputes shares based on whatever you pass.

Iceberg fill loop

IcebergExecutor only emits a new child when the previous one is fully filled. The user app must call report_fill(qty) so the algo knows when to step again.

from flox_py.execution_algos import IcebergExecutor

ice = IcebergExecutor(
    target_qty=100.0, side="sell", symbol=1,
    visible_qty=10.0, type="limit", price=68_500.0,
)
ice.step(now_ns, sim)         # submits 10.0
# ... fill observed ...
ice.report_fill(10.0)
ice.step(now_ns, sim)         # submits the next 10.0
# ... and so on until target_qty is reached

The last slice is naturally smaller than visible_qty if the remainder does not divide evenly.

POV market-volume tracking

POVExecutor chases a fixed share of observed market volume. You feed it volume, it decides how much to submit so that cumulative submitted is approximately participation_rate * cumulative observed volume.

from flox_py.execution_algos import POVExecutor

pov = POVExecutor(
    target_qty=50.0, side="buy", symbol=1,
    participation_rate=0.05,    # 5 percent
    min_slice_qty=0.5,           # do not submit anything smaller
)

def on_trade(ts_ns, sym, price, qty, is_buy):
    pov.observe_volume(qty)
    pov.step(ts_ns, sim)

min_slice_qty is the floor below which the algo holds back; useful when round-tripping tiny orders is wasteful.

What's not here yet

  • AdaptiveExecutor that switches strategies based on market conditions. The composition is non-trivial and this layer is intentionally simple.
  • C++ binding parity. The algo classes are Python-only today; Phase 2 will mirror them on the C++ ExecutionListener surface and through pybind11 / NAPI / Codon / QuickJS.
  • Backtest-fidelity tuning. Slippage and queue-aware fills already work through flox_py.SimulatedExecutor. Latency-aware fill timing pairs naturally with the latency_models module from W6.

See also

  • Backtest with realistic fills. The slippage and queue knobs that make these algos backtest-faithful.
  • Backtest with latency. Sample latencies you can apply between the algo's submit_order call and the simulator's matching tick.
  • Paper trading. The wrapper that drives an algo against a live data feed without touching real exchange credentials.