use crate::{DEFAULT_SYMBOL, state};
use clap::Args as ClapArgs;
use ibapi::{
Client,
accounts::{AccountSummaryResult, AccountSummaryTags, PositionUpdate, types::AccountGroup},
contracts::Contract,
market_data::IgnoreSize,
orders::{Action, OrderData, OrderUpdate, Orders},
prelude::{StreamExt, Subscription, SubscriptionItemStreamExt},
};
use std::{
collections::{HashMap, HashSet},
error::Error,
io,
sync::RwLock,
};
use time::{Duration, OffsetDateTime};
use uuid::Uuid;
const RUN_DELAY: tokio::time::Duration = tokio::time::Duration::from_secs(1);
const RETRY_DELAY: tokio::time::Duration = tokio::time::Duration::from_secs(10);
const DEFAULT_BUYING_POWER_BUFFER: f64 = 10.0_f64;
const DEFAULT_INITIAL_MARGIN_REQUIREMENT: f64 = 75.0_f64;
const BUY_DISCOUNT_PERCENT: f64 = 3.0_f64;
const SELL_MARKUP_PERCENT: f64 = 1.0_f64;
const BUY_ORDER_TTL: Duration = Duration::seconds(30);
const SELL_ORDER_TTL: Duration = Duration::seconds(300);
const CANCEL_RETRY_DELAY: Duration = Duration::seconds(10);
const ORDER_REF_PREFIX: &str = "stockholm:";
#[derive(ClapArgs)]
pub struct Args {
#[arg(long, default_value = DEFAULT_SYMBOL)]
symbol: String,
#[arg(
long,
default_value_t = DEFAULT_BUYING_POWER_BUFFER,
value_parser = parse_percent
)]
buying_power_buffer: f64,
#[arg(
long,
default_value_t = DEFAULT_INITIAL_MARGIN_REQUIREMENT,
value_parser = parse_positive_percent
)]
initial_margin_requirement: f64,
}
struct VolatileState {
open_orders: HashMap<i32, VolatileOrder>,
position_shares: Option<f64>,
equity_with_loan_value: Option<f64>,
init_margin_req: Option<f64>,
bid_price: Option<f64>,
ask_price: Option<f64>,
}
#[allow(dead_code)]
struct VolatileOrder {
order_ref: String,
symbol: String,
price: f64,
side: Side,
filled_shares: f64,
remaining_shares: f64,
}
#[derive(Clone, Copy, Eq, PartialEq)]
enum Side {
Buy,
Sell,
}
impl Default for Args {
fn default() -> Self {
Self {
symbol: DEFAULT_SYMBOL.to_string(),
buying_power_buffer: DEFAULT_BUYING_POWER_BUFFER,
initial_margin_requirement: DEFAULT_INITIAL_MARGIN_REQUIREMENT,
}
}
}
pub async fn run(address: &str, client_id: i32, args: &Args) -> Result<(), Box<dyn Error>> {
let persistent_state = RwLock::new(state::load().unwrap_or_else(|error| {
warn!("Unable to load state from disk. Proceeding with initial state. Details: {error}");
state::initial()
}));
loop {
match Client::connect(address, client_id).await {
Ok(client) => {
if let Err(error) = run_with_connection(&client, args, &persistent_state).await {
error!("{error}");
}
}
Err(error) => error!("Connection to Interactive Brokers Gateway failed: {error}"),
}
tokio::time::sleep(RETRY_DELAY).await;
}
}
async fn run_with_connection(
client: &Client,
args: &Args,
persistent_state: &RwLock<state::State>,
) -> Result<(), Box<dyn Error>> {
let volatile_state = RwLock::new(VolatileState {
open_orders: HashMap::new(),
position_shares: None,
equity_with_loan_value: None,
init_margin_req: None,
bid_price: None,
ask_price: None,
});
let order_updates = client.order_update_stream().await?;
list_orders(client, persistent_state, &volatile_state).await?;
tokio::try_join!(
control_loop(
client,
persistent_state,
&volatile_state,
&args.symbol,
args.buying_power_buffer,
args.initial_margin_requirement,
),
stream_account_summary(client, &volatile_state),
stream_order_updates(order_updates, persistent_state, &volatile_state),
stream_positions(client, &args.symbol, &volatile_state),
stream_tick_by_tick(client, &args.symbol, &volatile_state),
)?;
Ok(())
}
async fn control_loop(
client: &Client,
persistent_state: &RwLock<state::State>,
volatile_state: &RwLock<VolatileState>,
symbol: &str,
buying_power_buffer: f64,
initial_margin_requirement: f64,
) -> Result<(), Box<dyn Error>> {
loop {
run_step(
client,
persistent_state,
volatile_state,
symbol,
buying_power_buffer,
initial_margin_requirement,
)
.await?;
tokio::time::sleep(RUN_DELAY).await;
}
}
async fn stream_account_summary(
client: &Client,
state: &RwLock<VolatileState>,
) -> Result<(), Box<dyn Error>> {
debug!("Requesting account summary…");
let subscription = client
.account_summary(&AccountGroup("All".to_string()), AccountSummaryTags::ALL)
.await?;
let mut summaries = subscription.filter_data();
while let Some(update) = summaries.next().await {
match update? {
AccountSummaryResult::Summary(summary) => {
update_account_metric(state, &summary.tag, &summary.value)?;
if summary.currency.is_empty() {
debug!(
"Account summary for {}: {} = {}",
summary.account,
summary.tag,
summary.value,
);
} else {
debug!(
"Account summary for {}: {} = {} {}",
summary.account,
summary.tag,
summary.value,
summary.currency,
);
}
}
AccountSummaryResult::End => {
debug!("Finished listing initial account summary.");
}
}
}
Err(ibapi::Error::UnexpectedEndOfStream.into())
}
async fn stream_order_updates(
subscription: Subscription<OrderUpdate>,
persistent_state: &RwLock<state::State>,
volatile_state: &RwLock<VolatileState>,
) -> Result<(), Box<dyn Error>> {
let mut updates = subscription.filter_data();
debug!("Streaming order updates…");
while let Some(update) = updates.next().await {
let update: OrderUpdate = update?;
match &update {
OrderUpdate::OpenOrder(data) if data.order.order_ref.starts_with(ORDER_REF_PREFIX) => {
update_open_order(persistent_state, volatile_state, data)?;
}
OrderUpdate::OrderStatus(status) => {
update_order_status(
persistent_state,
volatile_state,
status.order_id,
status.filled,
status.remaining,
status.status.is_terminal(),
)?;
}
OrderUpdate::OpenOrder(_)
| OrderUpdate::ExecutionData(_)
| OrderUpdate::CommissionReport(_) => {}
}
debug!("Order update: {update:?}");
}
Err(ibapi::Error::UnexpectedEndOfStream.into())
}
async fn stream_positions(
client: &Client,
symbol: &str,
volatile_state: &RwLock<VolatileState>,
) -> Result<(), Box<dyn Error>> {
let subscription = client.positions().await?;
let mut updates = subscription.filter_data();
debug!("Streaming positions for {symbol}…");
while let Some(update) = updates.next().await {
let mut state = volatile_state
.write()
.map_err(|_| io::Error::other("Volatile state lock was poisoned."))?;
match update? {
PositionUpdate::Position(position) if position.contract.symbol == symbol => {
state.position_shares = Some(position.position);
}
PositionUpdate::Position(_) => {}
PositionUpdate::PositionEnd => {
state.position_shares.get_or_insert(0.0_f64);
}
}
}
Err(ibapi::Error::UnexpectedEndOfStream.into())
}
async fn stream_tick_by_tick(
client: &Client,
symbol: &str,
volatile_state: &RwLock<VolatileState>,
) -> Result<(), Box<dyn Error>> {
let contract = Contract::stock(symbol).build();
let subscription = client
.tick_by_tick(&contract, 0)
.bid_ask(IgnoreSize::Yes)
.await?;
let mut quotes = subscription.filter_data();
debug!("Streaming tick-by-tick quotes for {symbol}…");
while let Some(quote) = quotes.next().await {
let quote = quote?;
{
let mut state = volatile_state
.write()
.map_err(|_| io::Error::other("Volatile state lock was poisoned."))?;
state.bid_price = (quote.bid_price > 0.0_f64).then_some(quote.bid_price);
state.ask_price = (quote.ask_price > 0.0_f64).then_some(quote.ask_price);
}
debug!("Tick-by-tick quote for {symbol}: {quote:?}");
}
Err(ibapi::Error::UnexpectedEndOfStream.into())
}
async fn run_step(
client: &Client,
persistent_state: &RwLock<state::State>,
volatile_state: &RwLock<VolatileState>,
symbol: &str,
buying_power_buffer: f64,
initial_margin_requirement: f64,
) -> Result<(), Box<dyn Error>> {
cancel_expired_orders(client, persistent_state, volatile_state).await?;
let (buying_power, bid_price, ask_price, sellable_shares) = {
let state = volatile_state
.read()
.map_err(|_| io::Error::other("Volatile state lock was poisoned."))?;
let open_buy_order_value = calculate_open_buy_order_value(&state.open_orders);
let buying_power =
state
.equity_with_loan_value
.zip(state.init_margin_req)
.map(|(equity, margin)| {
calculate_buying_power(
equity,
margin,
buying_power_buffer,
initial_margin_requirement,
open_buy_order_value,
)
});
let reserved_sell_shares = calculate_open_sell_shares(&state.open_orders);
let sellable_shares = state
.position_shares
.map(|shares| (shares - reserved_sell_shares).max(0.0_f64).floor());
info!(
"Equity: {}; buying power: {}; open orders: {}; bid: {}; ask: {}",
state
.equity_with_loan_value
.map_or_else(|| "unavailable".to_string(), |equity| equity.to_string()),
buying_power.map_or_else(|| "unavailable".to_string(), |power| power.to_string()),
state.open_orders.len(),
state
.bid_price
.map_or_else(|| "unavailable".to_string(), |price| price.to_string()),
state
.ask_price
.map_or_else(|| "unavailable".to_string(), |price| price.to_string()),
);
(
buying_power,
state.bid_price,
state.ask_price,
sellable_shares,
)
};
if let (Some(buying_power), Some(bid_price)) = (buying_power, bid_price) {
let limit = round_down_to_cent(bid_price * (1.0_f64 - BUY_DISCOUNT_PERCENT / 100.0_f64));
if limit > 0.0_f64 {
let shares = (buying_power / limit).floor();
if shares >= 1.0_f64 {
place_limit_buy(
client,
symbol,
shares,
limit,
persistent_state,
volatile_state,
)
.await?;
}
}
}
if let (Some(ask_price), Some(sellable_shares)) = (ask_price, sellable_shares) {
let limit = round_up_to_cent(ask_price * (1.0_f64 + SELL_MARKUP_PERCENT / 100.0_f64));
if sellable_shares > 0.0_f64 {
place_limit_sell(
client,
symbol,
sellable_shares,
limit,
persistent_state,
volatile_state,
)
.await?;
}
}
Ok(())
}
async fn cancel_expired_orders(
client: &Client,
persistent_state: &RwLock<state::State>,
volatile_state: &RwLock<VolatileState>,
) -> Result<(), Box<dyn Error>> {
let volatile_orders = volatile_state
.read()
.map_err(|_| io::Error::other("Volatile state lock was poisoned."))?
.open_orders
.iter()
.map(|(&order_id, order)| (order.order_ref.clone(), (order_id, order.side)))
.collect::<HashMap<_, _>>();
let now = OffsetDateTime::now_utc();
let cancellation_attempts = {
let mut state = persistent_state
.write()
.map_err(|_| io::Error::other("The persistent state lock is poisoned."))?;
let mut attempts = Vec::new();
for order in &mut state.open_orders {
let Some(&(order_id, side)) = volatile_orders.get(&order.order_ref) else {
continue;
};
if cancellation_due(order, side, now) {
order.last_cancelled_at = Some(now);
attempts.push((order_id, order.order_ref.clone()));
}
}
if !attempts.is_empty() {
state::save(&state)?;
}
attempts
};
for (order_id, order_ref) in cancellation_attempts {
info!("Cancelling expired order {order_id} ({order_ref})…");
let _subscription = client.cancel_order(order_id, "").await?;
}
Ok(())
}
async fn place_limit_buy(
client: &Client,
symbol: &str,
shares: f64,
limit: f64,
persistent_state: &RwLock<state::State>,
volatile_state: &RwLock<VolatileState>,
) -> Result<(), Box<dyn Error>> {
let contract = Contract::stock(symbol).build();
let order_ref = format!("{ORDER_REF_PREFIX}{}", Uuid::new_v4().simple());
let mut order = client
.order(&contract)
.buy(shares)
.limit(limit)
.outside_rth()
.build()?;
order.order_ref.clone_from(&order_ref);
order.include_overnight = true;
let order_id = client.next_order_id();
{
let mut state = persistent_state
.write()
.map_err(|_| io::Error::other("The persistent state lock is poisoned."))?;
state.open_orders.push(state::OpenOrder {
order_ref: order_ref.clone(),
perm_id: None,
created_at: OffsetDateTime::now_utc(),
last_cancelled_at: None,
});
state::save(&state)?;
}
volatile_state
.write()
.map_err(|_| io::Error::other("Volatile state lock was poisoned."))?
.open_orders
.insert(
order_id,
VolatileOrder {
order_ref: order_ref.clone(),
symbol: symbol.to_string(),
price: limit,
side: Side::Buy,
filled_shares: 0.0_f64,
remaining_shares: shares,
},
);
client.submit_order(order_id, &contract, &order).await?;
info!("Submitted limit buy {order_id} ({order_ref}): {shares} {symbol} @ ${limit:.2}");
Ok(())
}
async fn place_limit_sell(
client: &Client,
symbol: &str,
shares: f64,
limit: f64,
persistent_state: &RwLock<state::State>,
volatile_state: &RwLock<VolatileState>,
) -> Result<(), Box<dyn Error>> {
let contract = Contract::stock(symbol).build();
let order_ref = format!("{ORDER_REF_PREFIX}{}", Uuid::new_v4().simple());
let mut order = client
.order(&contract)
.sell(shares)
.limit(limit)
.outside_rth()
.build()?;
order.order_ref.clone_from(&order_ref);
order.include_overnight = true;
let order_id = client.next_order_id();
{
let mut state = persistent_state
.write()
.map_err(|_| io::Error::other("The persistent state lock is poisoned."))?;
state.open_orders.push(state::OpenOrder {
order_ref: order_ref.clone(),
perm_id: None,
created_at: OffsetDateTime::now_utc(),
last_cancelled_at: None,
});
state::save(&state)?;
}
volatile_state
.write()
.map_err(|_| io::Error::other("Volatile state lock was poisoned."))?
.open_orders
.insert(
order_id,
VolatileOrder {
order_ref: order_ref.clone(),
symbol: symbol.to_string(),
price: limit,
side: Side::Sell,
filled_shares: 0.0_f64,
remaining_shares: shares,
},
);
client.submit_order(order_id, &contract, &order).await?;
info!("Submitted limit sell {order_id} ({order_ref}): {shares} {symbol} @ ${limit:.2}");
Ok(())
}
async fn list_orders(
client: &Client,
persistent_state: &RwLock<state::State>,
volatile_state: &RwLock<VolatileState>,
) -> Result<(), Box<dyn Error>> {
debug!("Requesting Stockholm open orders…");
let subscription = client.all_open_orders().await?;
let mut orders = subscription.filter_data();
let mut order_count: usize = 0;
let mut open_order_refs = HashSet::new();
while let Some(order) = orders.next().await {
match order? {
Orders::OrderData(data) if data.order.order_ref.starts_with(ORDER_REF_PREFIX) => {
order_count += 1;
open_order_refs.insert(data.order.order_ref.clone());
update_open_order(persistent_state, volatile_state, &data)?;
debug!("Stockholm open order: {data:?}");
}
Orders::OrderStatus(status) => {
update_order_status(
persistent_state,
volatile_state,
status.order_id,
status.filled,
status.remaining,
status.status.is_terminal(),
)?;
}
Orders::OrderData(_) => {}
}
}
{
let mut state = persistent_state
.write()
.map_err(|_| io::Error::other("The persistent state lock is poisoned."))?;
let previous_len = state.open_orders.len();
state
.open_orders
.retain(|order| open_order_refs.contains(&order.order_ref));
if state.open_orders.len() != previous_len {
state::save(&state)?;
}
}
volatile_state
.write()
.map_err(|_| io::Error::other("Volatile state lock was poisoned."))?
.open_orders
.retain(|_, order| open_order_refs.contains(&order.order_ref));
if order_count == 0 {
debug!("No Stockholm open orders found.");
} else if order_count == 1 {
debug!("Finished listing 1 Stockholm open order.");
} else {
debug!("Finished listing {order_count} Stockholm open orders.");
}
Ok(())
}
fn update_account_metric(state: &RwLock<VolatileState>, tag: &str, value: &str) -> io::Result<()> {
let Ok(value) = value.parse::<f64>() else {
return Ok(());
};
if !value.is_finite() {
return Ok(());
}
let mut state = state
.write()
.map_err(|_| io::Error::other("Volatile state lock was poisoned."))?;
match tag {
AccountSummaryTags::EQUITY_WITH_LOAN_VALUE => state.equity_with_loan_value = Some(value),
AccountSummaryTags::INIT_MARGIN_REQ => state.init_margin_req = Some(value),
_ => {}
}
Ok(())
}
fn update_open_order(
persistent_state: &RwLock<state::State>,
volatile_state: &RwLock<VolatileState>,
data: &OrderData,
) -> io::Result<()> {
{
let mut state = volatile_state
.write()
.map_err(|_| io::Error::other("Volatile state lock was poisoned."))?;
state.open_orders.insert(
data.order_id,
VolatileOrder {
order_ref: data.order.order_ref.clone(),
symbol: data.contract.symbol.to_string(),
price: data.order.limit_price.ok_or_else(|| {
io::Error::other("A Stockholm order is missing its limit price.")
})?,
side: match data.order.action {
Action::Buy => Side::Buy,
Action::Sell => Side::Sell,
Action::SellShort | Action::SellLong => {
return Err(io::Error::other(
"A Stockholm order has an unsupported institutional side.",
));
}
},
filled_shares: data.order.filled_quantity,
remaining_shares: data.order.total_quantity - data.order.filled_quantity,
},
);
}
{
let perm_id = (data.order.perm_id != 0).then_some(data.order.perm_id);
let mut state = persistent_state
.write()
.map_err(|_| io::Error::other("The persistent state lock is poisoned."))?;
let changed = if let Some(order) = state
.open_orders
.iter_mut()
.find(|order| order.order_ref == data.order.order_ref)
{
let changed = order.perm_id != perm_id;
order.perm_id = perm_id;
changed
} else {
state.open_orders.push(state::OpenOrder {
order_ref: data.order.order_ref.clone(),
perm_id,
created_at: OffsetDateTime::now_utc(),
last_cancelled_at: None,
});
true
};
if changed {
state::save(&state)?;
}
}
Ok(())
}
fn update_order_status(
persistent_state: &RwLock<state::State>,
volatile_state: &RwLock<VolatileState>,
order_id: i32,
filled_shares: f64,
remaining_shares: f64,
is_terminal: bool,
) -> io::Result<()> {
let terminal_order = {
let mut state = volatile_state
.write()
.map_err(|_| io::Error::other("Volatile state lock was poisoned."))?;
if is_terminal {
state.open_orders.remove(&order_id)
} else {
if let Some(order) = state.open_orders.get_mut(&order_id) {
order.filled_shares = filled_shares;
order.remaining_shares = remaining_shares;
}
None
}
};
if let Some(order) = terminal_order {
let mut state = persistent_state
.write()
.map_err(|_| io::Error::other("The persistent state lock is poisoned."))?;
let previous_len = state.open_orders.len();
state
.open_orders
.retain(|persistent_order| persistent_order.order_ref != order.order_ref);
if state.open_orders.len() != previous_len {
state::save(&state)?;
}
}
Ok(())
}
fn parse_positive_percent(value: &str) -> Result<f64, String> {
match value.parse::<f64>() {
Ok(value) if value > 0.0_f64 && value <= 100.0_f64 => Ok(value),
_ => Err("The percentage must be a finite number in the range (0, 100].".to_string()),
}
}
fn parse_percent(value: &str) -> Result<f64, String> {
match value.parse::<f64>() {
Ok(value) if (0.0_f64..=100.0_f64).contains(&value) => Ok(value),
_ => Err("The percentage must be a finite number in the range [0, 100].".to_string()),
}
}
fn calculate_open_buy_order_value(open_orders: &HashMap<i32, VolatileOrder>) -> f64 {
open_orders
.values()
.filter(|order| order.side == Side::Buy)
.map(|order| order.price * order.remaining_shares)
.sum()
}
fn calculate_open_sell_shares(open_orders: &HashMap<i32, VolatileOrder>) -> f64 {
open_orders
.values()
.filter(|order| order.side == Side::Sell)
.map(|order| order.remaining_shares)
.sum()
}
fn cancellation_due(order: &state::OpenOrder, side: Side, now: OffsetDateTime) -> bool {
let time_to_live = match side {
Side::Buy => BUY_ORDER_TTL,
Side::Sell => SELL_ORDER_TTL,
};
now >= order.created_at + time_to_live
&& order
.last_cancelled_at
.is_none_or(|last_cancelled_at| now >= last_cancelled_at + CANCEL_RETRY_DELAY)
}
fn calculate_buying_power(
equity_with_loan_value: f64,
init_margin_req: f64,
buying_power_buffer: f64,
initial_margin_requirement: f64,
open_buy_order_value: f64,
) -> f64 {
let margin_ratio = initial_margin_requirement / 100.0_f64;
let effective_equity = equity_with_loan_value * (1.0_f64 - buying_power_buffer / 100.0_f64);
let open_buy_order_margin = open_buy_order_value * margin_ratio;
let margin_capacity = (effective_equity - init_margin_req - open_buy_order_margin).max(0.0_f64);
round_down_to_cent(margin_capacity / margin_ratio)
}
fn round_down_to_cent(price: f64) -> f64 {
(price * 100.0_f64).floor() / 100.0_f64
}
fn round_up_to_cent(price: f64) -> f64 {
(price * 100.0_f64).ceil() / 100.0_f64
}
#[cfg(test)]
mod tests {
use super::{
Args, Side, VolatileOrder, VolatileState, calculate_buying_power,
calculate_open_buy_order_value, calculate_open_sell_shares, cancellation_due,
round_down_to_cent, round_up_to_cent, update_account_metric,
};
use crate::state;
use clap::Parser;
use ibapi::accounts::AccountSummaryTags;
use std::{collections::HashMap, sync::RwLock};
use time::{Duration, OffsetDateTime};
#[derive(Parser)]
struct TestCli {
#[command(flatten)]
args: Args,
}
#[test]
fn default_symbol() {
let cli = TestCli::try_parse_from(["run"]).unwrap();
assert_eq!(cli.args.symbol, "SOXL");
assert!((cli.args.buying_power_buffer - 10.0_f64).abs() < f64::EPSILON);
assert!((cli.args.initial_margin_requirement - 75.0_f64).abs() < f64::EPSILON);
}
#[test]
fn explicit_symbol() {
let cli = TestCli::try_parse_from(["run", "--symbol", "AAPL"]).unwrap();
assert_eq!(cli.args.symbol, "AAPL");
}
#[test]
fn validate_initial_margin_requirement() {
let valid = TestCli::try_parse_from(["run", "--initial-margin-requirement", "100"]);
let zero = TestCli::try_parse_from(["run", "--initial-margin-requirement", "0"]);
let excessive = TestCli::try_parse_from(["run", "--initial-margin-requirement", "100.1"]);
let nonfinite = TestCli::try_parse_from(["run", "--initial-margin-requirement", "NaN"]);
assert!((valid.unwrap().args.initial_margin_requirement - 100.0_f64).abs() < f64::EPSILON);
assert!(zero.is_err());
assert!(excessive.is_err());
assert!(nonfinite.is_err());
}
#[test]
fn validate_buying_power_buffer() {
let zero = TestCli::try_parse_from(["run", "--buying-power-buffer", "0"]);
let full = TestCli::try_parse_from(["run", "--buying-power-buffer", "100"]);
let excessive = TestCli::try_parse_from(["run", "--buying-power-buffer", "100.1"]);
let negative = TestCli::try_parse_from(["run", "--buying-power-buffer", "-1"]);
let nonfinite = TestCli::try_parse_from(["run", "--buying-power-buffer", "NaN"]);
assert!(zero.is_ok());
assert!(full.is_ok());
assert!(excessive.is_err());
assert!(negative.is_err());
assert!(nonfinite.is_err());
}
#[test]
fn buffer_and_round_down_buying_power() {
let buying_power = calculate_buying_power(1_000.0, 100.0, 20.0, 75.0, 0.0);
assert!((buying_power - 933.33_f64).abs() < f64::EPSILON);
}
#[test]
fn clamp_negative_buying_power() {
let buying_power = calculate_buying_power(1_000.0, 900.0, 20.0, 75.0, 0.0);
assert!(buying_power.abs() < f64::EPSILON);
}
#[test]
fn reserve_open_buy_order_margin() {
let buying_power = calculate_buying_power(1_000.0, 100.0, 20.0, 75.0, 200.0);
assert!((buying_power - 733.33_f64).abs() < f64::EPSILON);
}
#[test]
fn total_only_remaining_buy_orders() {
let open_orders = HashMap::from([
(
1_i32,
VolatileOrder {
order_ref: "stockholm:buy".to_string(),
symbol: "SOXL".to_string(),
price: 10.0,
side: Side::Buy,
filled_shares: 3.0,
remaining_shares: 2.0,
},
),
(
2_i32,
VolatileOrder {
order_ref: "stockholm:sell".to_string(),
symbol: "SOXL".to_string(),
price: 20.0,
side: Side::Sell,
filled_shares: 0.0,
remaining_shares: 4.0,
},
),
]);
assert!((calculate_open_buy_order_value(&open_orders) - 20.0_f64).abs() < f64::EPSILON);
assert!((calculate_open_sell_shares(&open_orders) - 4.0_f64).abs() < f64::EPSILON);
}
#[test]
fn round_limit_prices_conservatively() {
assert!((round_down_to_cent(10.129_f64) - 10.12_f64).abs() < f64::EPSILON);
assert!((round_up_to_cent(10.121_f64) - 10.13_f64).abs() < f64::EPSILON);
}
#[test]
fn retry_expired_order_cancellations() {
let now = OffsetDateTime::UNIX_EPOCH + Duration::seconds(30);
let mut order = state::OpenOrder {
order_ref: "stockholm:test".to_string(),
perm_id: None,
created_at: OffsetDateTime::UNIX_EPOCH,
last_cancelled_at: None,
};
assert!(cancellation_due(&order, Side::Buy, now));
order.last_cancelled_at = Some(now - Duration::seconds(9));
assert!(!cancellation_due(&order, Side::Buy, now));
order.last_cancelled_at = Some(now - Duration::seconds(10));
assert!(cancellation_due(&order, Side::Buy, now));
assert!(!cancellation_due(&order, Side::Sell, now));
}
#[test]
fn retain_only_valid_account_metrics() {
let state = RwLock::new(VolatileState {
open_orders: HashMap::new(),
position_shares: None,
equity_with_loan_value: None,
init_margin_req: None,
bid_price: None,
ask_price: None,
});
update_account_metric(&state, AccountSummaryTags::EQUITY_WITH_LOAN_VALUE, "1234.5")
.unwrap();
update_account_metric(&state, AccountSummaryTags::INIT_MARGIN_REQ, "234.5").unwrap();
update_account_metric(&state, AccountSummaryTags::INIT_MARGIN_REQ, "NaN").unwrap();
update_account_metric(&state, AccountSummaryTags::NET_LIQUIDATION, "9999").unwrap();
let state = state.read().unwrap();
assert_eq!(state.equity_with_loan_value, Some(1234.5_f64));
assert_eq!(state.init_margin_req, Some(234.5_f64));
}
}