1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
use crate::{
engine::state::{
EngineState,
asset::generate_empty_indexed_asset_states,
connectivity::generate_empty_indexed_connectivity_states,
instrument::generate_indexed_instrument_states,
order::Orders,
position::{OmsMode, PositionManager},
trading::TradingState,
},
statistic::summary::asset::BalanceBasis,
};
use chrono::{DateTime, Utc};
use fnv::FnvHashMap;
use rustrade_execution::balance::{AssetBalance, Balance};
use rustrade_instrument::{
Keyed,
asset::{AssetIndex, ExchangeAsset, name::AssetNameInternal},
exchange::{ExchangeId, ExchangeIndex},
index::IndexedInstruments,
instrument::{Instrument, InstrumentIndex},
};
use rustrade_integration::collection::snapshot::Snapshot;
use tracing::debug;
/// Builder utility for an [`EngineState`] instance.
#[derive(Debug, Clone)]
pub struct EngineStateBuilder<'a, GlobalData, FnInstrumentData> {
instruments: &'a IndexedInstruments,
trading_state: Option<TradingState>,
time_engine_start: Option<DateTime<Utc>>,
global: GlobalData,
balances: FnvHashMap<ExchangeAsset<AssetNameInternal>, Balance>,
instrument_data_init: FnInstrumentData,
/// OMS mode applied to every instrument's [`PositionManager`] at construction.
///
/// Defaults to [`OmsMode::Netting`]. Use [`OmsMode::Hedging`] for strategies that hold
/// simultaneous long and short positions on the same instrument (e.g. options writing).
oms_mode: OmsMode,
/// [`BalanceBasis`] applied to every asset's `TearSheetAssetGenerator` at construction.
///
/// Defaults to [`BalanceBasis::Gross`] (no change for existing/cash users). Use
/// [`BalanceBasis::NetAsset`] to compute drawdown and end-of-session balance from net asset
/// value on margin accounts — see its docs for the net-must-stay-positive precondition.
balance_basis: BalanceBasis,
}
impl<'a, GlobalData, FnInstrumentData> EngineStateBuilder<'a, GlobalData, FnInstrumentData> {
/// Construct a new `EngineStateBuilder` with a layout derived from [`IndexedInstruments`].
///
/// Note that the rest of the [`EngineState`] data can be generated from defaults if that
/// is all that is needed.
///
/// Note that `ConnectivityStates` will be generated with
/// [`generate_empty_indexed_connectivity_states`], defaulting to `Health::Reconnecting`.
pub fn new(
instruments: &'a IndexedInstruments,
global: GlobalData,
instrument_data_init: FnInstrumentData,
) -> Self {
Self {
instruments,
time_engine_start: None,
trading_state: None,
global,
balances: FnvHashMap::default(),
instrument_data_init,
oms_mode: OmsMode::Netting,
balance_basis: BalanceBasis::default(),
}
}
/// Optionally provide the initial `TradingState`.
///
/// Defaults to `TradingState::Disabled`.
pub fn trading_state(self, value: TradingState) -> Self {
Self {
trading_state: Some(value),
..self
}
}
/// Optionally provide the `time_engine_start`.
///
/// Providing this is useful for back-test scenarios where the time should be seeded with a
/// "historical" clock time (eg/ from first historical `MarketEvent`).
///
/// Defaults to `Utc::now`
pub fn time_engine_start(self, value: DateTime<Utc>) -> Self {
Self {
time_engine_start: Some(value),
..self
}
}
/// Optionally set the [`OmsMode`] for all instrument [`PositionManager`]s.
///
/// Defaults to [`OmsMode::Netting`] (at most one position per instrument, backward-compatible).
/// Set to [`OmsMode::Hedging`] for strategies that simultaneously hold long and short
/// positions on the same instrument (e.g. options writing alongside long positions).
///
/// # Note — `OmsMode::Hedging` and non-option instruments
///
/// `OmsMode` is applied uniformly to all instruments. Hedging mode is intended for
/// instruments where multiple concurrent positions are semantically valid (e.g. individual
/// options legs). Applying it to spot or futures instruments will track each order's fills
/// as a separate position slot (keyed by order ID) rather than a single net position,
/// which is almost certainly not what you want for those asset classes.
pub fn oms_mode(self, mode: OmsMode) -> Self {
Self {
oms_mode: mode,
..self
}
}
/// Optionally set the [`BalanceBasis`] for all asset `TearSheetAssetGenerator`s.
///
/// Defaults to [`BalanceBasis::Gross`] (drawdown and end-of-session balance computed from gross
/// `Balance::total`), which is unchanged behaviour for existing and cash users. Set to
/// [`BalanceBasis::NetAsset`] to compute them from net asset value (`total - borrowed`) on
/// margin accounts.
///
/// # Precondition (`NetAsset`)
/// Net-asset drawdown is only well-defined while net asset stays **strictly positive**; see
/// [`BalanceBasis::NetAsset`] for the silent-failure modes when it is not.
pub fn balance_basis(self, basis: BalanceBasis) -> Self {
Self {
balance_basis: basis,
..self
}
}
/// Optionally provide initial exchange asset `Balance`s.
///
/// Useful for back-test scenarios where seeding EngineState with initial `Balance`s is
/// required.
///
/// Note the internal implementation uses a `HashMap`, so duplicate
/// `ExchangeAsset<AssetNameInternal>` keys are overwritten.
pub fn balances<BalanceIter, KeyedBalance>(mut self, balances: BalanceIter) -> Self
where
BalanceIter: IntoIterator<Item = KeyedBalance>,
KeyedBalance: Into<Keyed<ExchangeAsset<AssetNameInternal>, Balance>>,
{
self.balances.extend(balances.into_iter().map(|keyed| {
let Keyed { key, value } = keyed.into();
(key, value)
}));
self
}
/// Use the builder data to generate the associated [`EngineState`].
///
/// If optional data is not provided (eg/ Balances), default values are used (eg/ zero Balance).
pub fn build<InstrumentData>(self) -> EngineState<GlobalData, InstrumentData>
where
FnInstrumentData: Fn(
&'a Keyed<InstrumentIndex, Instrument<Keyed<ExchangeIndex, ExchangeId>, AssetIndex>>,
) -> InstrumentData,
{
let Self {
instruments,
time_engine_start,
trading_state,
global,
balances,
instrument_data_init,
oms_mode,
balance_basis,
} = self;
// Default if not provided
let time_engine_start = time_engine_start.unwrap_or_else(|| {
debug!("EngineStateBuilder using Utc::now as time_engine_start default");
Utc::now()
});
let trading = trading_state.unwrap_or_default();
// Construct empty ConnectivityStates
let connectivity = generate_empty_indexed_connectivity_states(instruments);
// Update empty AssetStates from provided exchange asset Balances
let mut assets = generate_empty_indexed_asset_states(instruments, balance_basis);
for (key, balance) in balances {
assets
.asset_mut(&key)
.update_from_balance(Snapshot(&AssetBalance {
asset: key.asset,
balance,
time_exchange: time_engine_start,
}))
}
// Generate empty InstrumentStates using provided FnInstrumentData etc.
let instruments = generate_indexed_instrument_states(
instruments,
time_engine_start,
move || PositionManager::new(oms_mode),
Orders::default,
instrument_data_init,
);
EngineState {
trading,
global,
connectivity,
assets,
instruments,
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)] // Test code: panics on bad input are acceptable
mod tests {
use super::*;
use crate::{
engine::state::EngineState,
statistic::{summary::TradingSummaryGenerator, time::Annual365},
};
use rust_decimal::Decimal;
use rustrade_instrument::{Underlying, instrument::Instrument};
/// End-to-end seam: `EngineStateBuilder::balance_basis(NetAsset)` rides the asset generators
/// into the on-demand `TradingSummaryGenerator` (which clones `AssetState.statistics`) and is
/// stamped onto the output `TradingSummary.basis`.
#[test]
fn balance_basis_reaches_output_trading_summary() {
let instruments = IndexedInstruments::builder()
.add_instrument(Instrument::spot(
ExchangeId::BinanceSpot,
"binance_spot_btc_usdt",
"BTCUSDT",
Underlying::new("btc", "usdt"),
None,
))
.build();
let state: EngineState<(), ()> = EngineState::builder(&instruments, (), |_| ())
.balance_basis(BalanceBasis::NetAsset)
.build();
// Summary generator is built on-demand by cloning the per-asset generators, so the basis
// selected on the builder must surface on the generated summary.
let mut generator = TradingSummaryGenerator::init(
Decimal::ZERO,
DateTime::<Utc>::MIN_UTC,
DateTime::<Utc>::MIN_UTC,
&state.instruments,
&state.assets,
);
assert_eq!(generator.generate(Annual365).basis, BalanceBasis::NetAsset);
}
}