3. Write the strategy¶
The strategy is one header, examples/cpp/tutorial/first_mm.hpp.
Parameters¶
struct FirstMMParams {
FASTMM_PARAMS(FirstMMParams)
FASTMM_PARAM_BPS(
edge_bps, 5_bps, 0_bps, 500_bps, "distance of each quote from the microprice, bps")
FASTMM_PARAM(Qty, quote_qty, 0.001_qty, 0_qty, 1000_qty, "quantity per side, base units")
FASTMM_PARAM(
Qty, max_position, 0.004_qty, 0_qty, 1000_qty, "position limit, base units (0 = none)")
FASTMM_PARAM_MS(report_ms,
milliseconds(10000),
milliseconds(0),
milliseconds(3600000),
"interval of the status log line, ms (0 = off)")
// Checked after all parameters are applied: a quote larger than the limit could never be sent.
[[nodiscard]] std::optional<std::string> validate() const {
if (max_position.is_positive() && quote_qty > max_position)
return "quote_qty must not exceed max_position";
return std::nullopt;
}
};
Each FASTMM_PARAM* line declares a field with its default, range and description. The macro sets how the value is written in a configuration:
FASTMM_PARAM_BPSdeclares aRatioconfigured in basis points (edge_bps = 5.0).FASTMM_PARAM(Qty, ...)declares aQty(quote_qty = 0.001).FASTMM_PARAM_MSdeclares aDurationconfigured in whole milliseconds.
validate() rejects inconsistent combinations. The same declarations drive --list-strategies, --param key=value and configuration errors.
The quoting function¶
// The quotes for one book: integer arithmetic on plain values, no engine state.
[[nodiscard]] inline DesiredQuotes compute_quotes(const FirstMMParams& p,
const Instrument& inst,
Level best_bid,
Level best_ask,
Qty position) noexcept {
const Price micro = microprice(best_bid, best_ask);
const Price edge = micro * p.edge_bps; // Price * Ratio: exact, truncated toward zero
const Qty qty = inst.round_qty(p.quote_qty);
DesiredQuotes q;
if (inventory_allows(Side::Buy, position, qty, p.max_position))
q.bid(inst.round_price(micro - edge, Side::Buy), qty); // rounds down to the tick
if (inventory_allows(Side::Sell, position, qty, p.max_position))
q.ask(inst.round_price(micro + edge, Side::Sell), qty); // rounds up to the tick
keep_passive(q, best_bid.price, best_ask.price, inst.tick); // post-only quotes must not cross
return q;
}
micropriceweights the best bid and ask by the size on the opposite side, so it leans towards the side that is about to be taken.- Tick rounding never makes a quote more aggressive;
round_qtyrounds down to the lot. inventory_allowsdrops the side that would take the position pastmax_position.keep_passivemoves a crossing quote back one tick inside the touch; the venue rejects a crossing post-only order.
The hooks¶
The class derives from StrategyBase<FirstMMParams>, which stores the parameters (params()), and names itself with name(). on_start runs once before the first event and starts the status-line timer:
void on_start(auto& ctx) noexcept {
if (params().report_ms > Duration{}) report_timer_ = ctx.every(params().report_ms);
FASTMM_LOG_INFO("first_mm: started, quoting {}", ctx.quoting_enabled() ? "on" : "off");
}
on_book runs after each book update of a configured instrument. ctx is the strategy's view of the engine: books, positions, quotes, orders, timers:
void on_book(auto& ctx, InstrumentId id, const auto& book) noexcept {
if (!book.is_valid()) return ctx.pull_quotes(id); // empty or crossed
const DesiredQuotes q = compute_quotes(
params(), ctx.instrument(id), book.best_bid(), book.best_ask(), ctx.position(id).qty);
ctx.set_quotes(id, q); // the engine sends only the difference to the resting orders
}
on_fill runs for each of your executions, after the position and fees are updated:
void on_fill(auto& /*ctx*/, const Fill& fill) noexcept {
++fills_;
if (fill.late) ++late_fills_; // the order had already been cancelled
}
on_connection runs when a venue channel changes state. Off Live, the engine has already pulled that venue's quotes and, for market data, cleared its books:
void on_connection(auto& /*ctx*/, const ConnectionStateMsg& m) noexcept {
if (m.state == ConnState::Live) {
FASTMM_LOG_INFO("first_mm: venue {} channel {} is live", m.hdr.venue.value, m.channel);
return;
}
++disconnects_; // the engine has already pulled this venue's quotes
FASTMM_LOG_WARN("first_mm: venue {} channel {} is {}; quotes pulled",
m.hdr.venue.value,
m.channel,
to_string(m.state));
}
on_quoting runs when quoting is paused or resumed: an operator pull, the kill switch, or a reconciliation (a live session starts with one, so quoting comes on after on_start). On resume the strategy requotes without waiting for the next book update:
void on_quoting(auto& ctx, bool enabled) noexcept {
FASTMM_LOG_INFO("first_mm: quoting {}", enabled ? "on" : "off");
if (!enabled) return; // paused: the engine has pulled the quotes
for (const Instrument& inst : ctx.instruments()) on_book(ctx, inst.id, ctx.book(inst.id));
}
on_timer prints the status line. Logs are not journaled, so they must never change what the strategy does:
void on_timer(auto& ctx, TimerId id, std::uint64_t /*tag*/) noexcept {
if (id != report_timer_) return;
FASTMM_LOG_INFO("first_mm: fills={} late_fills={} disconnects={} net_pnl={}",
fills_,
late_fills_,
disconnects_,
ctx.portfolio().net);
}
Check the hooks early¶
If you misspell a hook's parameters, for example on_fill(auto& ctx, const OrderFillMsg& m), the build stops with fastmm: on_fill has the wrong signature or is not public; expected void on_fill(auto& ctx, const Fill& fill). A likely misspelling of a name, such as on_fills, is a warning. The Strategy API lists the hooks.
Next: 4. Unit-test it