FastMM documentation
FastMM C++ API 0.6.0
Public headers, tier 1 and tier 2
Loading...
Searching...
No Matches
position.hpp
Go to the documentation of this file.
1#pragma once
2// Position tracking, average-cost method (5.8). All arithmetic is integer with __int128
3// intermediates; realized/unrealized/fees feed the max-loss kill switch.
4#include "fastmm/core/config_macros.hpp"
7#include "fastmm/core/fx.hpp"
10
11#include <array>
12#include <cstddef>
13#include <cstdint>
14#include <memory>
15
16namespace fastmm {
17
18struct alignas(kCacheLine) Position {
19 Qty qty{}; // signed net position (+ long / - short)
20 Price avg_px{}; // average entry price of the open position
21 Notional realized{}; // closed PnL (before fees)
22 Notional unrealized{}; // mark-to-market of the open position at the last mark()
23 Notional fees{}; // cumulative fees paid (negative = rebates)
24 Price last_mark{};
25 Qty gross_traded{}; // sum of |fill qty|
26 std::uint32_t fills = 0;
27 std::uint32_t pad_ = 0;
29 [[nodiscard]] Notional net_pnl() const noexcept { return realized + unrealized - fees; }
30 [[nodiscard]] bool flat() const noexcept { return qty.is_zero(); }
31 [[nodiscard]] Qty abs_qty() const noexcept { return qty.abs(); }
32};
33static_assert(sizeof(Position) == 64 && std::is_trivially_copyable_v<Position>);
34
35// PnL totals over every instrument (StrategyContext::portfolio()), in the reporting currency when
36// the tracker converts (PositionTracker::set_accounting).
37struct Portfolio {
38 Notional realized{};
39 Notional unrealized{};
40 Notional fees{};
41 Notional net{}; // realized + unrealized - fees
42};
44class PositionTracker {
45 public:
46 [[nodiscard]] const Position& get(InstrumentId id) const noexcept {
47 FASTMM_ASSERT(id.value < kMaxInstruments);
48 return pos_[id.value];
49 }
50 [[nodiscard]] const Position& operator[](InstrumentId id) const noexcept { return get(id); }
51
52 // Applies a fill. PnL is scaled by the instrument's contract multiplier and, for an inverse
53 // (coin-margined) contract, computed in the base coin: qty * multiplier * (1/avg - 1/px). The
54 // average entry price of an inverse position is the size-weighted harmonic mean, which is what
55 // makes that exact.
56 void on_fill(InstrumentId id,
57 Side side,
59 Qty qty,
60 Notional fee,
61 const Instrument& inst) noexcept {
62 Position& p = pos_[id.value];
63 const std::int64_t f = side == Side::Buy ? qty.raw : -qty.raw;
64 const std::int64_t cur = p.qty.raw;
65 const bool inv = inst.inverse();
66 p.fees += fee;
67 fees_total_ += fee;
68 book(id, &Totals::fees, fee);
69 p.gross_traded += qty;
70 ++p.fills;
71 if (cur == 0 || (cur > 0) == (f > 0)) {
72 // Opening / adding: a new average over the old position and this fill.
73 const std::int64_t denom = cur + f;
74 p.avg_px = inv ? harmonic_avg(cur, p.avg_px, f, px, denom)
75 : arithmetic_avg(cur, p.avg_px, f, px, denom);
76 p.qty = Qty::from_raw(denom);
77 } else {
78 // Reducing / flipping: realise on the closed part in the position's direction.
79 const std::int64_t abs_cur = cur < 0 ? -cur : cur;
80 const std::int64_t abs_f = f < 0 ? -f : f;
81 const std::int64_t closed = abs_f < abs_cur ? abs_f : abs_cur;
82 const std::int64_t dir = cur > 0 ? 1 : -1;
83 Notional r;
84 if (inv) {
85 r = inst.inverse_pnl(p.avg_px, px, Qty::from_raw(closed * dir));
86 } else {
87 const Int128 pnl = static_cast<Int128>(px.raw - p.avg_px.raw) * closed * dir / kFixedScale;
88 r = scale(Notional::from_raw(static_cast<std::int64_t>(pnl)), inst);
89 }
90 p.realized += r;
91 realized_total_ += r;
92 book(id, &Totals::realized, r);
93 const std::int64_t remaining = abs_f - closed;
94 if (remaining > 0) { // flipped through zero: the remainder opens at px
95 p.qty = Qty::from_raw(f > 0 ? remaining : -remaining);
96 p.avg_px = px;
97 } else {
98 p.qty = Qty::from_raw(cur + f);
99 if (p.qty.is_zero()) p.avg_px = Price{};
100 }
101 }
102 if (p.last_mark.is_positive()) {
103 mark(id, p.last_mark, inst);
104 } else {
105 set_exposure(id, signed_notional(p, px, inst));
106 }
107 }
108
109 // A fill booked from a venue's cumulative quantity, at the order's own price and with no fee,
110 // turned out to be an execution at `px` costing `fee`. Its quantity is already in the position,
111 // so on_fill() would count it twice; what was never booked is what it cost. Paying `px` instead
112 // of `est_px` for `qty` is worth qty * (px - est_px), given up on a buy and received on a sell,
113 // and that difference is exactly the error the estimate left in this position's PnL. The average
114 // entry price keeps the estimate: moving it would need the position the fill was booked into,
115 // which is history, and the error it carries is the one this correction has already taken out.
116 void correct_fill(InstrumentId id,
117 Side side,
118 Price est_px,
119 Price px,
120 Qty qty,
121 Notional fee,
122 const Instrument& inst) noexcept {
123 Position& p = pos_[id.value];
124 const Notional diff =
125 inst.inverse()
126 ? inst.inverse_pnl(est_px, px, qty)
127 : scale(Notional::from_raw(detail::mul_div<kFixedScale>(px.raw - est_px.raw, qty.raw)),
128 inst);
129 const Notional adj = side == Side::Buy ? Notional{} - diff : diff;
130 p.realized += adj;
131 realized_total_ += adj;
132 book(id, &Totals::realized, adj);
133 p.fees += fee;
134 fees_total_ += fee;
135 book(id, &Totals::fees, fee);
136 }
137
138 // A funding payment on the instrument's position, in its settlement currency (negative paid,
139 // positive received). It is realized PnL, not a fee: it is not a cost of trading but the carry of
140 // holding the position, and a position that is never traded pays it all the same. funding() and
141 // total_funding() report the part of realized that it is.
142 void on_funding(InstrumentId id, Notional amount) noexcept {
143 Position& p = pos_[id.value];
144 p.realized += amount;
145 realized_total_ += amount;
146 book(id, &Totals::realized, amount);
147 fund_->per[id.value] += amount;
148 fund_->total += amount;
149 if (FASTMM_UNLIKELY(converting())) fund_->native[ccy_[id.value]] += amount;
150 }
151 // Funding booked on one instrument, in its settlement currency (part of its realized PnL).
152 [[nodiscard]] Notional funding(InstrumentId id) const noexcept {
153 FASTMM_ASSERT(id.value < kMaxInstruments);
154 return fund_->per[id.value];
155 }
156
157 // Mark-to-market the open position, in the instrument's settlement currency.
158 void mark(InstrumentId id, Price mid, const Instrument& inst) noexcept {
159 Position& p = pos_[id.value];
161 set_unrealized(id, p, revalue(p, mid, inst));
162 set_exposure(id, signed_notional(p, mid, inst));
163 }
164
165 // Reconciliation: overwrite qty/avg with the venue's view. Realized PnL and fees are history and
166 // are kept; the unrealized PnL of the new position is remeasured at the last mark, so the totals
167 // the max-loss budget reads stay consistent with the positions.
168 // A record without an average price (zero: the venue gave none, or none that could be read) keeps
169 // the tracked one when the position stays on the same side, else takes the last mark: valued at
170 // an average of 0, a short of 0.0014 BTC is a loss of its whole notional, which trips max_loss.
171 void set(InstrumentId id, Qty qty, Price avg_px, const Instrument& inst) noexcept {
172 Position& p = pos_[id.value];
173 if (!qty.is_zero() && !avg_px.is_positive()) {
174 const bool same_side = !p.qty.is_zero() && p.qty.is_negative() == qty.is_negative();
175 avg_px = same_side && p.avg_px.is_positive() ? p.avg_px : p.last_mark;
176 }
177 p.qty = qty;
178 p.avg_px = qty.is_zero() ? Price{} : avg_px;
179 set_unrealized(id, p, revalue(p, p.last_mark, inst));
180 set_exposure(id, signed_notional(p, p.last_mark, inst));
181 }
182 void reset(InstrumentId id) noexcept {
183 Position& p = pos_[id.value];
184 realized_total_ -= p.realized;
185 book(id, &Totals::realized, Notional{} - p.realized);
186 fees_total_ -= p.fees;
187 book(id, &Totals::fees, Notional{} - p.fees);
188 fund_->total -= fund_->per[id.value];
189 if (converting()) fund_->native[ccy_[id.value]] -= fund_->per[id.value];
190 fund_->per[id.value] = Notional{};
191 set_unrealized(id, p, Notional{});
192 set_exposure(id, Notional{});
193 p = Position{};
194 }
195
196 // Accounting across settlement currencies. With a plan of two or more currencies the tracker also
197 // keeps the totals per currency and converts them at the rate last set for each (set_rate), and
198 // the totals below are in the reporting currency. A currency whose rate was never set counts as
199 // zero. Without one (the default) nothing is converted and every Notional is added as it is.
200 void set_accounting(const FxPlan& plan) noexcept {
201 ccy_count_ = plan.count;
202 for (std::size_t i = 0; i < kMaxInstruments; ++i) ccy_[i] = plan.ccy[i];
203 for (auto& r : rate_) r = FxRate{};
204 rate_[0] = FxRate::identity();
205 for (auto& t : native_) t = Totals{};
206 for (auto& t : conv_) t = Totals{};
207 conv_total_ = Totals{};
208 fund_->native = {};
209 if (!converting()) return;
210 for (std::size_t i = 0; i < kMaxInstruments; ++i) fund_->native[ccy_[i]] += fund_->per[i];
211 // What is booked already (a tracker configured after fills) moves into its currencies.
212 for (std::size_t i = 0; i < kMaxInstruments; ++i) {
213 const Position& p = pos_[i];
214 Totals& t = native_[ccy_[i]];
215 t.realized += p.realized;
216 t.unrealized += p.unrealized;
217 t.fees += p.fees;
218 t.gross += exposure_[i].abs();
219 t.net += exposure_[i];
220 }
221 reconvert(0);
222 }
223 // The rate of currency `c` (an index of the plan): the totals in it are converted again.
224 void set_rate(std::size_t c, FxRate r) noexcept {
225 if (c == 0 || c >= kMaxCurrencies) return;
226 rate_[c] = r;
227 reconvert(c);
228 }
229 [[nodiscard]] bool converting() const noexcept { return ccy_count_ > 1; }
230 [[nodiscard]] std::size_t currency_count() const noexcept { return ccy_count_; }
231 [[nodiscard]] FxRate rate(std::size_t c) const noexcept {
232 return c < kMaxCurrencies ? rate_[c] : FxRate{};
234 // Totals of one currency, in that currency (converting() only).
235 struct Totals {
236 Notional realized{};
237 Notional unrealized{};
238 Notional fees{};
239 Notional gross{}; // sum of |position| at the last marks
240 Notional net{}; // signed sum
241 [[nodiscard]] Notional net_pnl() const noexcept { return realized + unrealized - fees; }
242 };
243 [[nodiscard]] const Totals& native(std::size_t c) const noexcept { return native_[c]; }
244
245 // Totals over every instrument, kept up to date by the updates above (the engine checks
246 // net_pnl() on every market-data event; summing kMaxInstruments positions there cost more
247 // than the rest of the event). Without conversion Notional carries no currency, so they are only
248 // meaningful when every instrument settles in the same one: InstrumentTable::settlement_mix()
249 // finds a table that mixes them, and fastmm-live refuses to start on one while [risk] max_loss
250 // is set and [accounting] does not cover it.
251 // Portfolio exposure at the last marks: the sum of |position| and the signed sum.
252 [[nodiscard]] Notional gross_exposure() const noexcept {
253 return FASTMM_UNLIKELY(converting()) ? conv_total_.gross : gross_exposure_;
255 [[nodiscard]] Notional net_exposure() const noexcept {
256 return FASTMM_UNLIKELY(converting()) ? conv_total_.net : net_exposure_;
258 [[nodiscard]] Notional total_realized() const noexcept {
259 return FASTMM_UNLIKELY(converting()) ? conv_total_.realized : realized_total_;
261 [[nodiscard]] Notional total_unrealized() const noexcept {
262 return FASTMM_UNLIKELY(converting()) ? conv_total_.unrealized : unrealized_total_;
264 [[nodiscard]] Notional total_fees() const noexcept {
265 return FASTMM_UNLIKELY(converting()) ? conv_total_.fees : fees_total_;
267 // Funding over every instrument; part of total_realized().
268 [[nodiscard]] Notional total_funding() const noexcept {
269 if (!converting()) return fund_->total;
271 for (std::size_t c = 0; c < ccy_count_; ++c) t += convert(fund_->native[c], rate_[c]);
272 return t;
273 }
274 // Funding of one currency, in that currency (converting() only).
275 [[nodiscard]] Notional native_funding(std::size_t c) const noexcept {
276 return c < kMaxCurrencies ? fund_->native[c] : Notional{};
278 [[nodiscard]] Notional net_pnl() const noexcept {
279 return total_realized() + total_unrealized() - total_fees();
281 [[nodiscard]] Portfolio portfolio() const noexcept {
282 Portfolio t;
283 t.realized = total_realized();
284 t.unrealized = total_unrealized();
285 t.fees = total_fees();
286 t.net = t.realized + t.unrealized - t.fees;
287 return t;
288 }
289
290 private:
291 static Notional scale(Notional n, const Instrument& inst) noexcept {
292 if (inst.contract_multiplier == Qty::from_int(1)) return n;
293 return Notional::from_raw(mul_raw(n, inst.contract_multiplier));
294 }
295 // (cur * avg + f * px) / (cur + f), in abs terms.
296 static Price arithmetic_avg(
297 std::int64_t cur, Price avg, std::int64_t f, Price px, std::int64_t denom) noexcept {
298 const Int128 num = static_cast<Int128>(cur) * avg.raw + static_cast<Int128>(f) * px.raw;
299 return Price::from_raw(static_cast<std::int64_t>(num / denom));
300 }
301 // An inverse contract costs multiplier / price coins, so the average that makes the coin PnL
302 // exact is the size-weighted harmonic mean: (cur + f) / (cur / avg + f / px).
303 static Price harmonic_avg(
304 std::int64_t cur, Price avg, std::int64_t f, Price px, std::int64_t denom) noexcept {
305 if (!px.is_positive()) return avg;
306 Int128 inv = static_cast<Int128>(f) * kFixedScale / px.raw;
307 if (cur != 0 && avg.is_positive()) inv += static_cast<Int128>(cur) * kFixedScale / avg.raw;
308 if (inv == 0) return px;
309 return Price::from_raw(
310 static_cast<std::int64_t>(static_cast<Int128>(denom) * kFixedScale / inv));
311 }
312 // Open-position PnL at `mark`, in the settlement currency.
313 // |position| valued at `mark`, carrying the position's sign.
314 static Notional signed_notional(const Position& p, Price mark, const Instrument& inst) noexcept {
315 if (p.qty.is_zero() || !mark.is_positive()) return Notional{};
316 const Notional n = inst.notional(mark, p.qty.abs());
317 return p.qty.raw < 0 ? Notional{} - n : n;
318 }
319 static Notional revalue(const Position& p, Price mark, const Instrument& inst) noexcept {
320 if (p.qty.is_zero() || !mark.is_positive()) return Notional{};
321 if (inst.inverse()) return inst.inverse_pnl(p.avg_px, mark, p.qty);
322 return scale(
323 Notional::from_raw(detail::mul_div<kFixedScale>(mark.raw - p.avg_px.raw, p.qty.raw)), inst);
324 }
325 // Every path that changes a position's quantity or its mark ends here, so the exposure totals
326 // cannot drift from the positions: one place updates both.
327 void set_exposure(InstrumentId id, Notional e) noexcept {
328 Notional& cur = exposure_[id.value];
329 const Notional dg = e.abs() - cur.abs();
330 const Notional dn = e - cur;
331 gross_exposure_ += dg;
332 net_exposure_ += dn;
333 book(id, &Totals::gross, dg);
334 book(id, &Totals::net, dn);
335 cur = e;
336 }
337 void set_unrealized(InstrumentId id, Position& p, Notional u) noexcept {
338 const Notional d = u - p.unrealized;
339 unrealized_total_ += d;
340 book(id, &Totals::unrealized, d);
341 p.unrealized = u;
342 }
343 // A change of one total in the instrument's currency, and of its converted value. Out of line:
344 // without [accounting] this is one predictable branch on the mark path.
345 FASTMM_FORCE_INLINE void book(InstrumentId id, Notional Totals::*f, Notional d) noexcept {
346 if (FASTMM_UNLIKELY(converting())) book_converted(id, f, d);
347 }
348 FASTMM_NOINLINE void book_converted(InstrumentId id, Notional Totals::*f, Notional d) noexcept {
349 const std::uint8_t c = ccy_[id.value];
350 Notional& n = native_[c].*f;
351 n += d;
352 const Notional v = convert(n, rate_[c]);
353 conv_total_.*f += v - conv_[c].*f;
354 conv_[c].*f = v;
355 }
356 void reconvert(std::size_t c) noexcept {
357 for (Notional Totals::*f :
358 {&Totals::realized, &Totals::unrealized, &Totals::fees, &Totals::gross, &Totals::net}) {
359 const Notional v = convert(native_[c].*f, rate_[c]);
360 conv_total_.*f += v - conv_[c].*f;
361 conv_[c].*f = v;
362 }
363 }
364 Position pos_[kMaxInstruments] = {};
365 Notional realized_total_{};
366 Notional unrealized_total_{};
367 Notional fees_total_{};
368 Notional exposure_[kMaxInstruments]{};
369 Notional gross_exposure_{};
370 Notional net_exposure_{};
371 std::uint8_t ccy_count_ = 0;
372 std::array<std::uint8_t, kMaxInstruments> ccy_{};
373 std::array<FxRate, kMaxCurrencies> rate_{FxRate::identity()};
374 std::array<Totals, kMaxCurrencies> native_{};
375 std::array<Totals, kMaxCurrencies> conv_{}; // native_ at rate_
376 Totals conv_total_{};
377 // Funding (on_funding), on the heap: it is rare, and the tracker's own layout, which the mark
378 // path reads, stays as it was. Copied with the tracker.
379 struct Funding {
380 Notional total{};
381 std::array<Notional, kMaxInstruments> per{};
382 std::array<Notional, kMaxCurrencies> native{}; // converting() only
383 };
384 struct FundingBox {
385 std::unique_ptr<Funding> p = std::make_unique<Funding>();
386 FundingBox() = default;
387 FundingBox(const FundingBox& o) : p(std::make_unique<Funding>(*o.p)) {}
388 FundingBox(FundingBox&&) noexcept = default;
389 FundingBox& operator=(const FundingBox& o) {
390 if (this != &o) *p = *o.p;
391 return *this;
392 }
393 FundingBox& operator=(FundingBox&&) noexcept = default;
394 ~FundingBox() = default;
395 Funding* operator->() const noexcept { return p.get(); }
396 };
397 FundingBox fund_;
398};
399
400} // namespace fastmm
Definition position.hpp:46
All engine enumerations.
Fixed<Tag>: int64 fixed point with a global 1e-8 scale (ADR-0001).
Instrument: the single definition of a tradable contract (5.3 / 5.15).
Definition strategy.hpp:16
constexpr std::int64_t kFixedScale
1e-8
Definition fixed_point.hpp:23
@ Funding
a perpetual funding payment booked by the venue (FundingMsg)
Side
Definition enums.hpp:12
Fixed< struct QtyTag > Qty
Definition fixed_point.hpp:333
Fixed< struct NotionalTag > Notional
Definition fixed_point.hpp:334
Fixed< struct PriceTag > Price
Definition fixed_point.hpp:332
constexpr std::size_t kMaxInstruments
Definition instrument.hpp:26
constexpr std::int64_t mul_raw(Fixed< A > a, Fixed< B > b) noexcept
Generic scaled multiply for fixed*fixed in one domain (e.g. qty * multiplier).
Definition fixed_point.hpp:367
constexpr Price mid(Price bid, Price ask) noexcept
Definition quoting.hpp:29
Strongly typed identifiers.
constexpr bool is_negative() const noexcept
Definition fixed_point.hpp:303
constexpr Fixed abs() const noexcept
Definition fixed_point.hpp:304
constexpr bool is_zero() const noexcept
Definition fixed_point.hpp:301
std::int64_t raw
Definition fixed_point.hpp:152
constexpr bool is_positive() const noexcept
Definition fixed_point.hpp:302
Definition instrument.hpp:29
PnL totals over every instrument (StrategyContext::portfolio()), in the reporting currency when the t...
Definition position.hpp:39
Notional realized
Definition position.hpp:40
Notional unrealized
Definition position.hpp:41
Notional fees
Definition position.hpp:42
Notional net
realized + unrealized - fees
Definition position.hpp:43
Totals of one currency, in that currency (converting() only).
Definition position.hpp:237
Definition position.hpp:20
Qty qty
signed net position (+ long / - short)
Definition position.hpp:21
Qty gross_traded
sum of |fill qty|
Definition position.hpp:27
std::uint32_t fills
Definition position.hpp:28
Notional fees
cumulative fees paid (negative = rebates)
Definition position.hpp:25
Price last_mark
Definition position.hpp:26
Price avg_px
average entry price of the open position
Definition position.hpp:22
Notional realized
closed PnL (before fees)
Definition position.hpp:23