1use std::{hint::spin_loop, path::PathBuf, range::RangeInclusive};
2
3use crate::{
4 backend::{DataBackend, OrderIdGenerator, OrdersBackend},
5 instrument::{InstrumentData, InstrumentSpec},
6 order::{ActiveOrderGoals, ActiveStateGoal, ApplyOrderStateUpdate, DeferredOrderActions},
7 order_manager::{OrderManager, OrdersCapacitySpec},
8 strategy::Strategy,
9 timestamp::{TickTimestamp, Timestamp},
10 volume::DirectionalExposure,
11};
12
13use bevy::log::{Level, debug, error, info, tracing, tracing_subscriber::FmtSubscriber};
14use clap::Parser;
15use time::{Date, Duration, Time, UtcDateTime, format_description::well_known::Iso8601};
16use tokio::{fs::File, io::BufReader};
17
18mod cli;
19use cli::*;
20
21pub async fn run<
22 IS: InstrumentSpec + Send,
23 DB: DataBackend<IS>,
24 OB: OrdersBackend<IS>,
25 OrdersCS: OrdersCapacitySpec,
26 S: Strategy<IS, OB, OrdersCS>,
27>(
28 strategy: S
29) {
30 tracing::subscriber::set_global_default(
31 FmtSubscriber::builder()
32 .with_max_level(Level::DEBUG)
33 .finish(),
34 )
35 .unwrap();
36
37 let CLI { key, command } = CLI::parse();
38
39 match command {
40 Commands::Backtest { start, end: last } => {
41 let Ok(start_date) = Date::parse(&start, &Iso8601::DATE) else {
42 error!(
43 "Invalid start date: \"{}\"",
44 start
45 );
46
47 return;
48 };
49
50 let Ok(last_date) = Date::parse(&last, &Iso8601::DATE) else {
51 error!(
52 "Invalid last date (inclusive): \"{}\"",
53 last
54 );
55
56 return;
57 };
58
59 let date_range_inclusive = RangeInclusive {
60 start: start_date,
61 last: last_date,
62 };
63
64 backtest::<IS, DB, OB, OrdersCS, S>(
65 strategy,
66 key.as_ref()
67 .map(|string| string.as_str()),
68 date_range_inclusive,
69 )
70 .await;
71 }
72 }
73}
74
75async fn backtest<
76 IS: InstrumentSpec + Send,
77 DB: DataBackend<IS>,
78 OB: OrdersBackend<IS>,
79 OrdersCS: OrdersCapacitySpec,
80 S: Strategy<IS, OB, OrdersCS>,
81>(
82 mut strategy: S,
83 key: Option<&str>,
84 date_range_inclusive: RangeInclusive<Date>,
85) {
86 let walk_start_timestamp = Timestamp::new(
87 UtcDateTime::new(
88 date_range_inclusive.start,
89 Time::MIDNIGHT,
90 )
91 .unix_timestamp_nanos(),
92 );
93
94 let walk_last_timestamp = Timestamp::new(
95 UtcDateTime::new(
96 date_range_inclusive.last + Duration::days(1),
97 Time::MIDNIGHT,
98 )
99 .unix_timestamp_nanos(),
100 );
101
102 let mut instrument_data = InstrumentData::<IS>::default();
103
104 let mut data_backend = DB::new(key);
105
106 let (mut order_state_update_receiver, mut orders_backend) = OB::new().await;
107
108 let mut order_manager = OrderManager::default();
109
110 let mut order_id_generator = OrderIdGenerator::new();
111
112 let mut deferred_order_actions = DeferredOrderActions::<IS>::default();
113
114 let mut directional_exposure = DirectionalExposure::<IS>::default();
115
116 let mut active_order_goals = ActiveOrderGoals::default();
117
118 let mut active_state_goal = ActiveStateGoal::default();
119
120 let start_julian_day = date_range_inclusive
121 .start
122 .to_julian_day();
123
124 let last_julian_day = date_range_inclusive
125 .last
126 .to_julian_day();
127
128 if start_julian_day > last_julian_day {
129 error!(
130 "Invalid start date \"{}\" (inclusive date, inclusive time at midnight) and/or last date \"{}\" (inclusive date, exclusive time at midnight).",
131 date_range_inclusive.start, date_range_inclusive.last,
132 );
133
134 return;
135 }
136
137 for julian_day in start_julian_day..=last_julian_day {
138 let Ok(date) = Date::from_julian_day(julian_day) else {
139 error!(
140 "Invalid start date \"{}\" or end date \"{}\".",
141 date_range_inclusive.start, date_range_inclusive.last,
142 );
143
144 return;
145 };
146
147 let mut iso_string_date = date
148 .format(&Iso8601::DATE)
149 .unwrap();
150
151 iso_string_date += DB::file_name_postpend();
152
153 let mut path_buf = PathBuf::new();
154
155 path_buf.push("market-data");
156 path_buf.push(IS::INSTRUMENT_KIND.as_str());
157 path_buf.push(IS::BARE_SYMBOL);
158 path_buf.push("trades");
159 path_buf.push(iso_string_date);
160
161 let Ok(file) = File::open(&path_buf).await else {
162 error!(
163 "Unable to open market data file: \"{:?}\".",
164 path_buf,
165 );
166
167 return;
168 };
169
170 let trades = data_backend
171 .stream_in(BufReader::new(file))
172 .await;
173
174 info!(
175 "Adding {} binned trades from {:?}",
176 trades.len(),
177 path_buf,
178 );
179
180 instrument_data.new_trades(trades);
181 }
182
183 instrument_data.initialize_walk(walk_start_timestamp);
184
185 let mut tick_timestamp = TickTimestamp::new(walk_start_timestamp);
186
187 strategy
188 .initialize(
189 &tick_timestamp,
190 &instrument_data,
191 &mut order_manager,
192 &directional_exposure,
193 &active_order_goals,
194 &active_state_goal,
195 &mut deferred_order_actions,
196 &mut order_id_generator,
197 )
198 .await;
199
200 loop {
201 while let Ok(order_state_update) = order_state_update_receiver.try_recv() {
202 order_state_update
203 .apply(
204 &tick_timestamp,
205 &mut order_manager,
206 &mut orders_backend,
207 &mut order_id_generator,
208 &mut directional_exposure,
209 &mut deferred_order_actions,
210 &mut active_order_goals,
211 &mut active_state_goal,
212 )
213 .await
214 .unwrap();
215 }
216
217 deferred_order_actions
218 .trigger(
219 &tick_timestamp,
220 &mut order_manager,
221 &mut orders_backend,
222 &mut order_id_generator,
223 &mut directional_exposure,
224 &mut active_order_goals,
225 &mut active_state_goal,
226 )
227 .await
228 .unwrap();
229
230 let Some(walk_timestamp) = instrument_data
231 .walk::<100_000_000>(&walk_last_timestamp)
232 .await
233 .cloned()
234 else {
235 error!("Walk loop terminating due to walk rejection.");
236 break;
237 };
238
239 orders_backend
240 .trades_stream(instrument_data.iter_recent_latest_trades_forward())
241 .await;
242
243 order_manager
244 .pending_client_orders
245 .tick_client_orders(
246 &mut deferred_order_actions,
247 &mut directional_exposure,
248 instrument_data.iter_recent_latest_trades_forward(),
249 );
250
251 deferred_order_actions
252 .trigger(
253 &tick_timestamp,
254 &mut order_manager,
255 &mut orders_backend,
256 &mut order_id_generator,
257 &mut directional_exposure,
258 &mut active_order_goals,
259 &mut active_state_goal,
260 )
261 .await
262 .unwrap();
263
264 tick_timestamp = TickTimestamp::new(walk_timestamp);
265
266 strategy
267 .data_update(
268 &tick_timestamp,
269 &instrument_data,
270 &order_manager,
271 &directional_exposure,
272 &active_order_goals,
273 &active_state_goal,
274 &mut deferred_order_actions,
275 &mut order_id_generator,
276 )
277 .await;
278
279 deferred_order_actions
280 .trigger(
281 &tick_timestamp,
282 &mut order_manager,
283 &mut orders_backend,
284 &mut order_id_generator,
285 &mut directional_exposure,
286 &mut active_order_goals,
287 &mut active_state_goal,
288 )
289 .await
290 .unwrap();
291
292 if walk_timestamp == walk_last_timestamp {
293 info!("Walk loop successfully terminating.");
294 break;
295 }
296
297 spin_loop();
298 }
299}