apple-quant-algorithmic 0.1.0

Apple Quant's algorithmic trading api
Documentation
use std::{hint::spin_loop, path::PathBuf, range::RangeInclusive};

use crate::{
	backend::{DataBackend, OrderIdGenerator, OrdersBackend},
	instrument::{InstrumentData, InstrumentSpec},
	order::{ActiveOrderGoals, ActiveStateGoal, ApplyOrderStateUpdate, DeferredOrderActions},
	order_manager::{OrderManager, OrdersCapacitySpec},
	strategy::Strategy,
	timestamp::{TickTimestamp, Timestamp},
	volume::DirectionalExposure,
};

use bevy::log::{Level, debug, error, info, tracing, tracing_subscriber::FmtSubscriber};
use clap::Parser;
use time::{Date, Duration, Time, UtcDateTime, format_description::well_known::Iso8601};
use tokio::{fs::File, io::BufReader};

mod cli;
use cli::*;

pub async fn run<
	IS: InstrumentSpec + Send,
	DB: DataBackend<IS>,
	OB: OrdersBackend<IS>,
	OrdersCS: OrdersCapacitySpec,
	S: Strategy<IS, OB, OrdersCS>,
>(
	strategy: S
) {
	tracing::subscriber::set_global_default(
		FmtSubscriber::builder()
			.with_max_level(Level::DEBUG)
			.finish(),
	)
	.unwrap();

	let CLI { key, command } = CLI::parse();

	match command {
		Commands::Backtest { start, end: last } => {
			let Ok(start_date) = Date::parse(&start, &Iso8601::DATE) else {
				error!(
					"Invalid start date: \"{}\"",
					start
				);

				return;
			};

			let Ok(last_date) = Date::parse(&last, &Iso8601::DATE) else {
				error!(
					"Invalid last date (inclusive): \"{}\"",
					last
				);

				return;
			};

			let date_range_inclusive = RangeInclusive {
				start: start_date,
				last: last_date,
			};

			backtest::<IS, DB, OB, OrdersCS, S>(
				strategy,
				key.as_ref()
					.map(|string| string.as_str()),
				date_range_inclusive,
			)
			.await;
		}
	}
}

async fn backtest<
	IS: InstrumentSpec + Send,
	DB: DataBackend<IS>,
	OB: OrdersBackend<IS>,
	OrdersCS: OrdersCapacitySpec,
	S: Strategy<IS, OB, OrdersCS>,
>(
	mut strategy: S,
	key: Option<&str>,
	date_range_inclusive: RangeInclusive<Date>,
) {
	let walk_start_timestamp = Timestamp::new(
		UtcDateTime::new(
			date_range_inclusive.start,
			Time::MIDNIGHT,
		)
		.unix_timestamp_nanos(),
	);

	let walk_last_timestamp = Timestamp::new(
		UtcDateTime::new(
			date_range_inclusive.last + Duration::days(1),
			Time::MIDNIGHT,
		)
		.unix_timestamp_nanos(),
	);

	let mut instrument_data = InstrumentData::<IS>::default();

	let mut data_backend = DB::new(key);

	let (mut order_state_update_receiver, mut orders_backend) = OB::new().await;

	let mut order_manager = OrderManager::default();

	let mut order_id_generator = OrderIdGenerator::new();

	let mut deferred_order_actions = DeferredOrderActions::<IS>::default();

	let mut directional_exposure = DirectionalExposure::<IS>::default();

	let mut active_order_goals = ActiveOrderGoals::default();

	let mut active_state_goal = ActiveStateGoal::default();

	let start_julian_day = date_range_inclusive
		.start
		.to_julian_day();

	let last_julian_day = date_range_inclusive
		.last
		.to_julian_day();

	if start_julian_day > last_julian_day {
		error!(
			"Invalid start date \"{}\" (inclusive date, inclusive time at midnight) and/or last date \"{}\" (inclusive date, exclusive time at midnight).",
			date_range_inclusive.start, date_range_inclusive.last,
		);

		return;
	}

	for julian_day in start_julian_day..=last_julian_day {
		let Ok(date) = Date::from_julian_day(julian_day) else {
			error!(
				"Invalid start date \"{}\" or end date \"{}\".",
				date_range_inclusive.start, date_range_inclusive.last,
			);

			return;
		};

		let mut iso_string_date = date
			.format(&Iso8601::DATE)
			.unwrap();

		iso_string_date += DB::file_name_postpend();

		let mut path_buf = PathBuf::new();

		path_buf.push("market-data");
		path_buf.push(IS::INSTRUMENT_KIND.as_str());
		path_buf.push(IS::BARE_SYMBOL);
		path_buf.push("trades");
		path_buf.push(iso_string_date);

		let Ok(file) = File::open(&path_buf).await else {
			error!(
				"Unable to open market data file: \"{:?}\".",
				path_buf,
			);

			return;
		};

		let trades = data_backend
			.stream_in(BufReader::new(file))
			.await;

		info!(
			"Adding {} binned trades from {:?}",
			trades.len(),
			path_buf,
		);

		instrument_data.new_trades(trades);
	}

	instrument_data.initialize_walk(walk_start_timestamp);

	let mut tick_timestamp = TickTimestamp::new(walk_start_timestamp);

	strategy
		.initialize(
			&tick_timestamp,
			&instrument_data,
			&mut order_manager,
			&directional_exposure,
			&active_order_goals,
			&active_state_goal,
			&mut deferred_order_actions,
			&mut order_id_generator,
		)
		.await;

	loop {
		while let Ok(order_state_update) = order_state_update_receiver.try_recv() {
			order_state_update
				.apply(
					&tick_timestamp,
					&mut order_manager,
					&mut orders_backend,
					&mut order_id_generator,
					&mut directional_exposure,
					&mut deferred_order_actions,
					&mut active_order_goals,
					&mut active_state_goal,
				)
				.await
				.unwrap();
		}

		deferred_order_actions
			.trigger(
				&tick_timestamp,
				&mut order_manager,
				&mut orders_backend,
				&mut order_id_generator,
				&mut directional_exposure,
				&mut active_order_goals,
				&mut active_state_goal,
			)
			.await
			.unwrap();

		let Some(walk_timestamp) = instrument_data
			.walk::<100_000_000>(&walk_last_timestamp)
			.await
			.cloned()
		else {
			error!("Walk loop terminating due to walk rejection.");
			break;
		};

		orders_backend
			.trades_stream(instrument_data.iter_recent_latest_trades_forward())
			.await;

		order_manager
			.pending_client_orders
			.tick_client_orders(
				&mut deferred_order_actions,
				&mut directional_exposure,
				instrument_data.iter_recent_latest_trades_forward(),
			);

		deferred_order_actions
			.trigger(
				&tick_timestamp,
				&mut order_manager,
				&mut orders_backend,
				&mut order_id_generator,
				&mut directional_exposure,
				&mut active_order_goals,
				&mut active_state_goal,
			)
			.await
			.unwrap();

		tick_timestamp = TickTimestamp::new(walk_timestamp);

		strategy
			.data_update(
				&tick_timestamp,
				&instrument_data,
				&order_manager,
				&directional_exposure,
				&active_order_goals,
				&active_state_goal,
				&mut deferred_order_actions,
				&mut order_id_generator,
			)
			.await;

		deferred_order_actions
			.trigger(
				&tick_timestamp,
				&mut order_manager,
				&mut orders_backend,
				&mut order_id_generator,
				&mut directional_exposure,
				&mut active_order_goals,
				&mut active_state_goal,
			)
			.await
			.unwrap();

		if walk_timestamp == walk_last_timestamp {
			info!("Walk loop successfully terminating.");
			break;
		}

		spin_loop();
	}
}