nautilus-interactive-brokers 0.62.0

Interactive Brokers integration adapter for the Nautilus trading engine
Documentation
// -------------------------------------------------------------------------------------------------
//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
//  https://nautechsystems.io
//
//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
//  You may not use this file except in compliance with the License.
//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
// -------------------------------------------------------------------------------------------------

use ibapi::orders::{Order as IBOrder, TimeInForce};
use nautilus_model::{
    enums::{OrderSide, OrderType as NautilusOrderType, TrailingOffsetType},
    instruments::Instrument,
    orders::{Order as NautilusOrder, any::OrderAny},
};

use super::{convert_price, format_ib_datetime, trigger_type_to_ib_trigger_method};
use crate::providers::instruments::InteractiveBrokersInstrumentProvider;

pub(super) fn apply_expire_time_policy(ib_order: &mut IBOrder, order: &OrderAny) {
    if matches!(ib_order.tif, TimeInForce::GoodTilDate)
        && let Some(expire) = order.expire_time()
    {
        ib_order.good_till_date = format_ib_datetime(expire);
    }
}

pub(super) fn apply_account_policy(ib_order: &mut IBOrder, order: &OrderAny) {
    if let Some(account_id) = order.account_id() {
        ib_order.account = account_id.to_string();
    }
}

pub(super) fn apply_quantity_policy(
    ib_order: &mut IBOrder,
    order: &OrderAny,
    instrument_provider: &InteractiveBrokersInstrumentProvider,
) -> anyhow::Result<()> {
    if let Some(instrument) = instrument_provider.find(&order.instrument_id())
        && instrument.is_inverse()
        && order.is_quote_quantity()
    {
        // IBKR accepts a cash quantity (`cash_qty`) only for BUY orders on these instruments
        // (e.g. PAXOS crypto); a SELL must use the base/coin quantity (`total_quantity`).
        if order.order_side() != OrderSide::Buy {
            anyhow::bail!(
                "Interactive Brokers only accepts a quote quantity (`cash_qty`) for BUY orders; \
                 a SELL must use the base quantity"
            );
        }
        ib_order.cash_qty = Some(order.quantity().as_f64());
        ib_order.total_quantity = 0.0;
    }
    Ok(())
}

pub(super) fn apply_trailing_order_policy(
    ib_order: &mut IBOrder,
    order: &OrderAny,
    price_magnifier: f64,
) -> anyhow::Result<()> {
    if !matches!(
        order.order_type(),
        NautilusOrderType::TrailingStopMarket | NautilusOrderType::TrailingStopLimit
    ) {
        return Ok(());
    }

    if let Some(trailing_offset) = order.trailing_offset() {
        let trailing_offset_f64 = trailing_offset.to_string().parse::<f64>().map_err(|e| {
            anyhow::anyhow!("Failed to convert trailing offset {trailing_offset} to f64: {e}")
        })?;

        match order.trailing_offset_type() {
            Some(TrailingOffsetType::BasisPoints) => {
                ib_order.trailing_percent = Some(trailing_offset_f64 / 100.0);
            }
            Some(TrailingOffsetType::Price | TrailingOffsetType::NoTrailingOffset) | None => {
                ib_order.aux_price = Some(trailing_offset_f64);
            }
            Some(other) => anyhow::bail!("`TrailingOffsetType` {:?} is not supported", other),
        }
    }

    if let Some(trigger_price) = order.trigger_price() {
        let converted_trigger = convert_price(trigger_price, price_magnifier);
        ib_order.trail_stop_price = Some(converted_trigger);
        ib_order.trigger_method = order
            .trigger_type()
            .map(trigger_type_to_ib_trigger_method)
            .unwrap_or_default();
    }

    Ok(())
}

pub(super) fn apply_display_quantity_policy(ib_order: &mut IBOrder, order: &OrderAny) {
    if let Some(display_qty) = order.display_qty() {
        ib_order.display_size = Some(display_qty.as_f64() as i32);
    }
}

pub(super) fn apply_order_list_policy(_ib_order: &mut IBOrder, _order: &OrderAny) {
    // Order lists only control parent/transmit behavior at the execution client layer.
    // IB OCA groups must be requested explicitly through IB order tags.
}