Skip to main content

apple_quant_algorithmic/
frontend.rs

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}