FastMM documentation
FastMM C++ API 0.6.0
Public headers, tier 1 and tier 2
Loading...
Searching...
No Matches
strategy_harness.hpp
Go to the documentation of this file.
1#pragma once
2// StrategyHarness<S>: test a strategy's hooks against the real engine (ADR-0012, section 8).
3//
4// The harness owns an Engine<S, SimClock, SimTransport, InlineFeed>, a simulated venue (the
5// matching engine, fixed order and ack latency) and a virtual clock. Market data goes straight to
6// the engine; the venue only sees orders, so our orders fill only through fill(). Every call runs
7// the engine until it is idle, so the strategy's reaction is visible when the call returns:
8//
9// fastmm::sim::StrategyHarness<MyMM> h({{"half_spread_bps", "10"}});
10// h.book("100.00", "100.02"); // snapshot: on_book runs, quotes go out
11// h.advance(milliseconds(1)); // the orders reach the venue and their acks come back
12// REQUIRE(h.working_orders().size() == 2);
13// h.fill(Side::Buy); // a taker at the venue fills our best bid; on_fill runs
14// CHECK(h.engine().position(h.instrument()).qty.is_positive());
15// h.pull_quotes(); // on_quoting(false); resume_quotes() gives on_quoting(true)
16// h.publish({{"half_spread_bps", "7.5"}}); // a ParamUpdate: new values, then on_params
17//
18// Not for the hot path: the helpers allocate, and the harness is single-threaded.
19#include "fastmm/core/engine.hpp"
22#include "fastmm/core/time.hpp"
23#include "fastmm/sim/matching_engine.hpp"
24#include "fastmm/sim/sim_transport.hpp"
27
28#include <algorithm>
29#include <cstddef>
30#include <cstdint>
31#include <cstring>
32#include <memory>
33#include <stdexcept>
34#include <string>
35#include <string_view>
36#include <vector>
37
38namespace fastmm::sim {
39
40// BTCUSDT on venue 0: tick 0.01, lot 0.001.
41[[nodiscard]] inline InstrumentTable harness_instruments() {
44 i.symbol = "BTCUSDT";
45 i.venue = VenueId{0};
46 i.flags = Instrument::kEnabled;
47 i.tick = *Price::from_decimal("0.01");
48 i.lot = *Qty::from_decimal("0.001");
49 i.min_qty = i.lot;
50 static_cast<void>(t.add(i));
51 return t;
52}
53
54struct HarnessOptions {
55 InstrumentTable instruments = harness_instruments();
58 c.quotes.min_requote_interval = Duration{}; // requote on every change
59 return c;
60 }();
61 Duration latency = microseconds(100); // order out and ack back, each way
62 Timestamp start{seconds(1'700'000'000).ns};
63};
65template <class S>
66class StrategyHarness {
67 public:
68 using EngineType = Engine<S, SimClock, SimTransport, InlineFeed>;
69
70 // Configures the strategy with `params` (std::invalid_argument on a bad value), then warms up
71 // and starts the engine (on_start runs here).
72 explicit StrategyHarness(const ParamMap& params = {}, const HarnessOptions& options = {})
73 : options_(options),
74 clock_(options_.start),
75 transport_(
76 std::make_unique<SimTransport>(clock_, options_.instruments, transport_config())),
77 feed_(std::make_unique<InlineFeed>(1U << 20)),
78 strategy_(std::make_unique<S>()) {
79 if constexpr (requires { strategy_->configure(params); }) {
80 if (auto err = strategy_->configure(params)) throw std::invalid_argument(*err);
81 } else {
82 if (!params.empty()) throw std::invalid_argument("strategy takes no parameters");
83 }
84 engine_ = std::make_unique<EngineType>(
85 options_.engine, options_.instruments, clock_, *transport_, *feed_, *strategy_);
86 engine_->warm_up();
87 engine_->start();
88 }
89
90 // ---- market data (delivered to the engine at the current time) ------------------------------
91
92 // A one-level snapshot each side.
93 void book(Price bid, Price ask, Qty size = Qty::from_int(1), InstrumentId id = InstrumentId{0}) {
94 std::byte* p = feed_->reserve(BookDeltaMsg::size_for(1, 1));
95 if (p == nullptr) throw std::length_error("StrategyHarness: feed full");
96 auto* d = reinterpret_cast<BookDeltaMsg*>(p);
97 std::memset(p, 0, BookDeltaMsg::size_for(1, 1));
99 d->hdr.flags |= EventHeader::kSnapshot;
100 stamp(d->hdr);
101 d->bid_count = d->ask_count = 1;
102 d->last_update_id = ++seq_;
103 d->levels()[0] = Level{bid, size};
104 d->levels()[1] = Level{ask, size};
105 feed_->commit();
106 drain();
107 }
108 void book(std::string_view bid, std::string_view ask, InstrumentId id = InstrumentId{0}) {
109 book(price(bid), price(ask), Qty::from_int(1), id);
110 }
111 void trade(Price px, Qty qty, Side aggressor, InstrumentId id = InstrumentId{0}) {
113 init_header(t, EventType::Trade, id, venue(id));
114 t.price = px;
115 t.qty = qty;
116 t.aggressor = aggressor;
117 t.trade_id = ++seq_;
118 push(t.hdr);
119 }
120
121 // ---- orders ---------------------------------------------------------------------------------
122
123 // Fills our best working order on `side` (quantity 0: its whole remainder) with a taker order
124 // at the venue, then advances by the ack latency so the fill reaches the strategy. False when
125 // there is no working order on that side.
126 bool fill(Side side, Qty qty = Qty{}, InstrumentId id = InstrumentId{0}) {
127 const std::vector<Order> orders = working_orders(id);
128 const auto it = std::find_if(
129 orders.begin(), orders.end(), [side](const Order& o) { return o.side == side; });
130 if (it == orders.end()) return false;
131 NewOrder taker;
132 taker.account = kTakerAccount;
133 taker.cl_ord_id = ClientOrderId{++taker_seq_};
134 taker.instrument = id;
135 taker.side = opposite(side);
136 taker.type = OrderType::Limit;
137 taker.tif = TimeInForce::Ioc;
138 taker.price = it->price;
139 taker.qty = qty.is_positive() ? qty : it->leaves_qty();
140 static_cast<void>(transport_->matching_engine().submit(taker, clock_.now()));
141 advance(options_.latency);
142 return true;
143 }
144 // Our working orders (Live or PartiallyFilled): bids best first, then asks best first.
145 [[nodiscard]] std::vector<Order> working_orders(InstrumentId id = InstrumentId{0}) const {
146 std::vector<Order> v;
147 engine_->oms().for_each_open_order(id, [&](Handle<Order>, const Order& o) {
148 if (o.is_working()) v.push_back(o);
149 });
150 std::sort(v.begin(), v.end(), [](const Order& a, const Order& b) {
151 if (a.side != b.side) return a.side == Side::Buy;
152 return a.side == Side::Buy ? a.price > b.price : a.price < b.price;
153 });
154 return v;
155 }
156
157 // ---- venue and control events ---------------------------------------------------------------
158
159 // Channel 0 is market data (the engine clears the venue's books), 1 is order entry.
160 void disconnect(std::uint8_t channel = 0, VenueId v = VenueId{0}) {
161 connection(ConnState::Disconnected, channel, v);
163 void reconnect(std::uint8_t channel = 0, VenueId v = VenueId{0}) {
164 connection(ConnState::Live, channel, v);
168
169 // A ParamUpdate of `values` (names and values as in [strategy.params]) for all instruments, at
170 // the current time: the strategy's parameters change and on_params runs. Validated against the
171 // strategy's current parameters; std::invalid_argument when a publisher would reject it.
172 void publish(const std::vector<ParamPublisher::ParamValue>& values) {
173 ParamUpdateMsg m{};
174 if constexpr (requires { strategy_->params(); }) {
175 const ParamPublisher pub(ParamSink{}, strategy_->params());
176 if (auto err = pub.build(values, ParamPublisher::kAllInstruments, m))
177 throw std::invalid_argument(*err);
178 } else if (!values.empty()) {
179 throw std::invalid_argument("strategy takes no parameters");
180 } else {
182 }
183 push(m.hdr);
184 }
185
186 // Any other message, delivered to the engine at the current time.
187 void push(EventHeader& h) {
188 stamp(h);
189 if (!feed_->push(h)) throw std::length_error("StrategyHarness: feed full");
190 drain();
191 }
192
193 // ---- time -------------------------------------------------------------------------------------
194
195 // Moves virtual time forward by `d`, delivering orders to the venue, acks and fills to the engine
196 // and firing timers in time order.
197 void advance(Duration d) {
198 const Timestamp until = clock_.now() + d;
199 for (;;) {
200 const Timestamp t_ord = transport_->next_order_arrival();
201 const Timestamp t_in = transport_->next_inbound_ts();
202 const Timestamp t_timer = engine_->timers().next_expiry();
203 Timestamp t = t_ord;
204 if (t_in < t) t = t_in;
205 if (t_timer < t) t = t_timer;
206 if (t > until) break;
207 if (t > clock_.now()) clock_.set(t);
208 if (t == t_ord) {
209 transport_->process_order_arrival();
210 } else if (t == t_in) {
211 static_cast<void>(transport_->deliver_next_inbound(*feed_));
212 drain();
213 } else if (engine_->step() == 0 && engine_->timers().next_expiry() <= clock_.now()) {
214 clock_.set(clock_.now() + Duration{1}); // the wheel fires on its next tick
215 }
216 }
217 clock_.set(until);
218 drain();
219 }
220 [[nodiscard]] Timestamp now() const noexcept { return clock_.now(); }
222 // ---- access -----------------------------------------------------------------------------------
224 [[nodiscard]] EngineType& engine() noexcept { return *engine_; }
225 [[nodiscard]] S& strategy() noexcept { return *strategy_; }
226 [[nodiscard]] SimTransport& venue_transport() noexcept { return *transport_; }
227 [[nodiscard]] static InstrumentId instrument() noexcept { return InstrumentId{0}; }
228
229 [[nodiscard]] static Price price(std::string_view s) {
230 auto r = Price::from_decimal(s);
231 if (!r) throw std::invalid_argument("StrategyHarness: bad price " + std::string(s));
232 return *r;
233 }
234 [[nodiscard]] static Qty quantity(std::string_view s) {
235 auto r = Qty::from_decimal(s);
236 if (!r) throw std::invalid_argument("StrategyHarness: bad quantity " + std::string(s));
237 return *r;
238 }
239
240 private:
241 static constexpr AccountId kTakerAccount = 7;
242
243 [[nodiscard]] SimTransportConfig transport_config() const {
244 SimTransportConfig c;
245 c.order_out = LatencyParams{options_.latency, Duration{}};
246 c.ack_in = LatencyParams{options_.latency, Duration{}};
247 c.md_in = LatencyParams{};
248 return c;
249 }
250 [[nodiscard]] VenueId venue(InstrumentId id) const {
251 return options_.instruments.contains(id) ? options_.instruments.get(id).venue : VenueId{0};
252 }
253 void stamp(EventHeader& h) const noexcept {
254 h.recv_ts = clock_.now();
255 h.exch_ts = clock_.now();
256 h.t0_cycles = clock_.cycles();
257 }
258 void connection(ConnState state, std::uint8_t channel, VenueId v) {
259 ConnectionStateMsg m{};
261 m.state = state;
262 m.channel = channel;
263 push(m.hdr);
264 }
265 void control(ControlCommand command) {
266 ControlMsg c{};
268 c.command = command;
269 push(c.hdr);
270 }
271 void drain() {
272 while (engine_->step() > 0) {
273 }
274 }
275
276 HarnessOptions options_;
277 SimClock clock_;
278 std::unique_ptr<SimTransport> transport_;
279 std::unique_ptr<InlineFeed> feed_;
280 std::unique_ptr<S> strategy_;
281 std::unique_ptr<EngineType> engine_;
282 std::uint64_t seq_ = 0;
283 std::uint64_t taker_seq_ = 0;
284};
285
286} // namespace fastmm::sim
Dense-by-id table built at startup; symbol lookup is a sorted FlatMap keyed by (venue,...
Definition instrument.hpp:141
bool contains(InstrumentId id) const noexcept
Definition instrument.hpp:163
const Instrument & get(InstrumentId id) const noexcept
Definition instrument.hpp:164
Result< InstrumentId, InstrumentError > add(const Instrument &inst) noexcept
Definition instrument.hpp:149
Definition param_publisher.hpp:57
std::optional< std::string > build(const std::vector< ParamValue > &values, InstrumentId inst, ParamUpdateMsg &out) const
The update publish() would send, without sending it or changing the copy; the error message when it i...
Definition param_publisher.hpp:122
static constexpr InstrumentId kAllInstruments
Definition param_publisher.hpp:60
Cycles cycles() const noexcept
Definition time.hpp:343
void set(Timestamp t) noexcept
Definition time.hpp:352
Timestamp now() const noexcept
Definition time.hpp:342
Definition strategy_harness.hpp:68
static Price price(std::string_view s)
Definition strategy_harness.hpp:226
static InstrumentId instrument() noexcept
Definition strategy_harness.hpp:224
Engine< S, SimClock, SimTransport, InlineFeed > EngineType
Definition strategy_harness.hpp:70
void trade(Price px, Qty qty, Side aggressor, InstrumentId id=InstrumentId{0})
Definition strategy_harness.hpp:112
EngineType & engine() noexcept
Definition strategy_harness.hpp:221
void resume_quotes()
Definition strategy_harness.hpp:166
void disconnect(std::uint8_t channel=0, VenueId v=VenueId{0})
Channel 0 is market data (the engine clears the venue's books), 1 is order entry.
Definition strategy_harness.hpp:159
void book(std::string_view bid, std::string_view ask, InstrumentId id=InstrumentId{0})
Definition strategy_harness.hpp:109
void push(EventHeader &h)
Any other message, delivered to the engine at the current time.
Definition strategy_harness.hpp:186
void publish(const std::vector< ParamPublisher::ParamValue > &values)
A ParamUpdate of values (names and values as in [strategy.params]) for all instruments,...
Definition strategy_harness.hpp:171
SimTransport & venue_transport() noexcept
Definition strategy_harness.hpp:223
void advance(Duration d)
Moves virtual time forward by d, delivering orders to the venue, acks and fills to the engine and fir...
Definition strategy_harness.hpp:195
void reconnect(std::uint8_t channel=0, VenueId v=VenueId{0})
Definition strategy_harness.hpp:162
void book(Price bid, Price ask, Qty size=Qty::from_int(1), InstrumentId id=InstrumentId{0})
A one-level snapshot each side.
Definition strategy_harness.hpp:94
std::vector< Order > working_orders(InstrumentId id=InstrumentId{0}) const
Our working orders (Live or PartiallyFilled): bids best first, then asks best first.
Definition strategy_harness.hpp:145
S & strategy() noexcept
Definition strategy_harness.hpp:222
Timestamp now() const noexcept
Definition strategy_harness.hpp:218
bool fill(Side side, Qty qty=Qty{}, InstrumentId id=InstrumentId{0})
Fills our best working order on side (quantity 0: its whole remainder) with a taker order at the venu...
Definition strategy_harness.hpp:126
void pull_quotes()
Definition strategy_harness.hpp:165
static Qty quantity(std::string_view s)
Definition strategy_harness.hpp:231
Instrument: the single definition of a tradable contract (5.3 / 5.15).
Event bus message structs (5.5).
Definition strategy_harness.hpp:40
InstrumentTable harness_instruments()
BTCUSDT on venue 0: tick 0.01, lot 0.001.
Definition strategy_harness.hpp:43
ConnState
Definition enums.hpp:404
@ ParamUpdate
new strategy parameter values (ParamUpdateMsg)
constexpr Side opposite(Side s) noexcept
Definition enums.hpp:13
constexpr void init_header(M &m, EventType type, InstrumentId inst={}, VenueId venue={}, std::uint32_t len=static_cast< std::uint32_t >(sizeof(M))) noexcept
For fixed-size messages len defaults to sizeof(M); BookDeltaMsg passes size_for().
Definition messages.hpp:75
Side
Definition enums.hpp:12
StrongId< struct InstrumentTag, std::uint32_t > InstrumentId
Dense index into the engine's instrument table.
Definition strong_id.hpp:38
Fixed< struct PriceTag > Price
Definition fixed_point.hpp:332
constexpr Duration seconds(std::int64_t v) noexcept
Definition time.hpp:41
constexpr Duration microseconds(std::int64_t v) noexcept
Definition time.hpp:35
ControlCommand
Definition enums.hpp:447
StrongId< struct VenueTag, std::uint8_t > VenueId
Definition strong_id.hpp:39
std::map< std::string, std::string > ParamMap
Definition params.hpp:119
ParamPublisher: builds ParamUpdate messages for a running engine, off the engine thread (ADR-0013).
Strategy concept and the StrategyBase helper (8.6).
Bids then asks follow the fixed part as a flexible array of Level (bid_count + ask_count).
Definition messages.hpp:99
static constexpr std::uint32_t size_for(std::uint32_t bids, std::uint32_t asks) noexcept
Definition messages.hpp:107
Definition time.hpp:22
std::int64_t ns
Definition time.hpp:23
Definition engine_config.hpp:21
QuoteParams quotes
Definition engine_config.hpp:74
Definition messages.hpp:37
@ kSnapshot
BookDelta carries a full snapshot.
Definition messages.hpp:41
constexpr bool is_positive() const noexcept
Definition fixed_point.hpp:302
static constexpr Fixed from_int(std::int64_t units) noexcept
Whole units, e.g. from_int(100) == 100.00000000.
Definition fixed_point.hpp:161
static constexpr std::optional< Fixed > from_decimal(std::string_view s) noexcept
Exact parse of "[+-]digits[.digits]".
Definition fixed_point.hpp:174
Definition strong_id.hpp:54
Definition instrument.hpp:29
@ kEnabled
Definition instrument.hpp:31
VenueId venue
1
Definition instrument.hpp:43
Symbol symbol
21
Definition instrument.hpp:56
One price level. Shared by book messages and the L2 book.
Definition messages.hpp:30
Definition order.hpp:15
constexpr bool is_working() const noexcept
Definition order.hpp:49
Where a built update goes; push returns false when it was not accepted (full).
Definition param_publisher.hpp:45
New strategy parameter values (ADR-0013): up to kMaxFields (field index, raw value) pairs,...
Definition messages.hpp:574
Duration min_requote_interval
Definition quote_manager.hpp:93
Definition time.hpp:45
Definition messages.hpp:140
Definition strategy_harness.hpp:56
InstrumentTable instruments
Definition strategy_harness.hpp:57
Duration latency
order out and ack back, each way
Definition strategy_harness.hpp:63
EngineConfig engine
Definition strategy_harness.hpp:58
Timestamp start
Definition strategy_harness.hpp:64
Time primitives.