use std::{
collections::{BTreeMap, HashMap},
fmt,
num::NonZeroU64,
panic::{self, AssertUnwindSafe},
sync::{Arc, OnceLock},
};
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use futures::FutureExt;
use uuid::Uuid;
use lnm_sdk::api_v3::{
error::TradeValidationError,
models::{
ClientId, Leverage, Margin, Percentage, PercentageCapped, Price, Quantity, SATS_PER_BTC,
Trade, TradeSide, TradeSize, trade_util,
},
};
use crate::{
db::models::OhlcCandleRow,
error::Result as GeneralResult,
shared::{Lookback, MinIterationInterval},
signal::Signal,
util::DateTimeExt,
};
use super::error::{TradeCoreError, TradeCoreResult, TradeExecutorResult};
impl crate::sealed::Sealed for Trade {}
pub trait TradeCore: crate::sealed::Sealed + Send + Sync + fmt::Debug + 'static {
fn id(&self) -> Uuid;
fn side(&self) -> TradeSide;
fn opening_fee(&self) -> u64;
fn closing_fee(&self) -> u64;
fn maintenance_margin(&self) -> i64;
fn quantity(&self) -> Quantity;
fn margin(&self) -> Margin;
fn leverage(&self) -> Leverage;
fn price(&self) -> Price;
fn liquidation(&self) -> Price;
fn stoploss(&self) -> Option<Price>;
fn takeprofit(&self) -> Option<Price>;
fn exit_price(&self) -> Option<Price>;
fn created_at(&self) -> DateTime<Utc>;
fn filled_at(&self) -> Option<DateTime<Utc>>;
fn closed_at(&self) -> Option<DateTime<Utc>>;
fn closed(&self) -> bool;
fn client_id(&self) -> Option<&ClientId>;
}
impl TradeCore for Trade {
fn id(&self) -> Uuid {
self.id()
}
fn side(&self) -> TradeSide {
self.side()
}
fn opening_fee(&self) -> u64 {
self.opening_fee()
}
fn closing_fee(&self) -> u64 {
self.closing_fee()
}
fn maintenance_margin(&self) -> i64 {
self.maintenance_margin()
}
fn quantity(&self) -> Quantity {
self.quantity()
}
fn margin(&self) -> Margin {
self.margin()
}
fn leverage(&self) -> Leverage {
self.leverage()
}
fn price(&self) -> Price {
self.price()
}
fn liquidation(&self) -> Price {
self.liquidation()
}
fn stoploss(&self) -> Option<Price> {
self.stoploss()
}
fn takeprofit(&self) -> Option<Price> {
self.takeprofit()
}
fn exit_price(&self) -> Option<Price> {
self.exit_price()
}
fn created_at(&self) -> DateTime<Utc> {
self.created_at()
}
fn filled_at(&self) -> Option<DateTime<Utc>> {
self.filled_at()
}
fn closed_at(&self) -> Option<DateTime<Utc>> {
self.closed_at()
}
fn closed(&self) -> bool {
self.closed()
}
fn client_id(&self) -> Option<&ClientId> {
self.client_id()
}
}
pub trait TradeRunning: TradeCore {
fn est_pl(&self, market_price: Price) -> f64;
fn est_max_additional_margin(&self) -> u64 {
if self.leverage() == Leverage::MIN {
return 0;
}
let max_margin = Margin::calculate(self.quantity(), self.price(), Leverage::MIN);
max_margin.as_u64().saturating_sub(self.margin().as_u64())
}
fn est_max_cash_in(&self, market_price: Price) -> u64 {
let extractable_pl = self.est_pl(market_price).max(0.) as u64;
let min_margin = Margin::calculate(self.quantity(), self.price(), Leverage::MAX);
let excess_margin = self.margin().as_u64().saturating_sub(min_margin.as_u64());
excess_margin + extractable_pl
}
fn est_collateral_delta_for_liquidation(
&self,
target_liquidation: Price,
market_price: Price,
) -> Result<i64, TradeValidationError> {
trade_util::evaluate_collateral_delta_for_liquidation(
self.side(),
self.quantity(),
self.margin(),
self.price(),
self.liquidation(),
target_liquidation,
market_price,
)
}
}
impl TradeRunning for Trade {
fn est_pl(&self, market_price: Price) -> f64 {
trade_util::estimate_pl(self.side(), self.quantity(), self.price(), market_price)
}
}
pub trait TradeClosed: TradeCore {
fn pl(&self) -> i64;
}
impl TradeClosed for Trade {
fn pl(&self) -> i64 {
self.pl()
}
}
pub type TradeReference = (DateTime<Utc>, Uuid);
#[derive(Debug)]
pub struct RunningTradesMap<T: TradeRunning + ?Sized> {
trades: BTreeMap<TradeReference, (Arc<T>, Option<TradeTrailingStoploss>)>,
id_to_time: HashMap<Uuid, DateTime<Utc>>,
}
pub type DynRunningTradesMap = RunningTradesMap<dyn TradeRunning>;
impl<T: TradeRunning + ?Sized> RunningTradesMap<T> {
pub(super) fn new() -> Self {
Self {
trades: BTreeMap::new(),
id_to_time: HashMap::new(),
}
}
pub fn is_empty(&self) -> bool {
self.trades.is_empty()
}
pub(super) fn add(&mut self, trade: Arc<T>, trade_tsl: Option<TradeTrailingStoploss>) {
self.id_to_time.insert(trade.id(), trade.created_at());
self.trades
.insert((trade.created_at(), trade.id()), (trade, trade_tsl));
}
pub fn len(&self) -> usize {
self.id_to_time.len()
}
pub fn contains(&self, trade_id: &Uuid) -> bool {
self.id_to_time.contains_key(trade_id)
}
pub fn get_by_id(&self, id: Uuid) -> Option<&(Arc<T>, Option<TradeTrailingStoploss>)> {
self.id_to_time
.get(&id)
.and_then(|creation_ts| self.trades.get(&(*creation_ts, id)))
}
pub(super) fn get_by_id_mut(
&mut self,
id: Uuid,
) -> Option<&mut (Arc<T>, Option<TradeTrailingStoploss>)> {
self.id_to_time
.get(&id)
.and_then(|creation_ts| self.trades.get_mut(&(*creation_ts, id)))
}
pub fn iter(
&self,
) -> impl Iterator<Item = (&TradeReference, &(Arc<T>, Option<TradeTrailingStoploss>))> {
self.trades.iter()
}
pub fn keys(&self) -> impl Iterator<Item = &TradeReference> {
self.trades.keys()
}
pub fn values(&self) -> impl Iterator<Item = &(Arc<T>, Option<TradeTrailingStoploss>)> {
self.trades.values()
}
pub fn trades_desc(&self) -> impl Iterator<Item = &(Arc<T>, Option<TradeTrailingStoploss>)> {
self.trades.iter().rev().map(|(_, trade_tuple)| trade_tuple)
}
pub(super) fn trades_desc_mut(
&mut self,
) -> impl Iterator<Item = &mut (Arc<T>, Option<TradeTrailingStoploss>)> {
self.trades
.iter_mut()
.rev()
.map(|(_, trade_tuple)| trade_tuple)
}
}
impl<T: TradeRunning> RunningTradesMap<T> {
pub(super) fn into_dyn(self) -> DynRunningTradesMap {
let dyn_trades = self
.trades
.into_iter()
.map(|(key, (trade, stoploss))| {
let dyn_trade: Arc<dyn TradeRunning> = trade;
(key, (dyn_trade, stoploss))
})
.collect();
RunningTradesMap {
trades: dyn_trades,
id_to_time: self.id_to_time,
}
}
}
impl<T: TradeRunning + ?Sized> Clone for RunningTradesMap<T> {
fn clone(&self) -> Self {
Self {
trades: self.trades.clone(),
id_to_time: self.id_to_time.clone(),
}
}
}
impl<'a, T: TradeRunning + ?Sized> IntoIterator for &'a RunningTradesMap<T> {
type Item = (
&'a TradeReference,
&'a (Arc<T>, Option<TradeTrailingStoploss>),
);
type IntoIter = std::collections::btree_map::Iter<
'a,
TradeReference,
(Arc<T>, Option<TradeTrailingStoploss>),
>;
fn into_iter(self) -> Self::IntoIter {
self.trades.iter()
}
}
#[derive(Debug, Clone)]
struct RunningStats {
long_len: usize,
long_margin: u64,
long_quantity: u64,
short_len: usize,
short_margin: u64,
short_quantity: u64,
pl: i64,
fees: u64,
}
#[derive(Debug, Clone)]
pub struct TradingState {
last_tick_time: DateTime<Utc>,
balance: u64,
market_price: Price,
last_trade_time: Option<DateTime<Utc>>,
running_map: DynRunningTradesMap,
running_stats: OnceLock<RunningStats>,
funding_fees: i64,
realized_pl: i64,
closed_history: Arc<ClosedTradeHistory>,
closed_fees: u64,
}
impl TradingState {
#[allow(clippy::too_many_arguments)]
pub(super) fn new(
last_tick_time: DateTime<Utc>,
balance: u64,
market_price: Price,
last_trade_time: Option<DateTime<Utc>>,
running_map: DynRunningTradesMap,
funding_fees: i64,
realized_pl: i64,
closed_history: Arc<ClosedTradeHistory>,
closed_fees: u64,
) -> Self {
Self {
last_tick_time,
balance,
market_price,
last_trade_time,
running_map,
running_stats: OnceLock::new(),
funding_fees,
realized_pl,
closed_history,
closed_fees,
}
}
fn get_running_stats(&self) -> &RunningStats {
self.running_stats.get_or_init(|| {
let mut long_len = 0;
let mut long_margin = 0;
let mut long_quantity = 0;
let mut short_len = 0;
let mut short_margin = 0;
let mut short_quantity = 0;
let mut pl = 0;
let mut fees = 0;
for (trade, _) in self.running_map.trades_desc() {
match trade.side() {
TradeSide::Buy => {
long_len += 1;
long_margin +=
trade.margin().as_u64() + trade.maintenance_margin().max(0) as u64;
long_quantity += trade.quantity().as_u64();
}
TradeSide::Sell => {
short_len += 1;
short_margin +=
trade.margin().as_u64() + trade.maintenance_margin().max(0) as u64;
short_quantity += trade.quantity().as_u64();
}
}
pl += trade.est_pl(self.market_price).floor() as i64;
fees += trade.opening_fee();
}
RunningStats {
long_len,
long_margin,
long_quantity,
short_len,
short_margin,
short_quantity,
pl,
fees,
}
})
}
pub fn last_tick_time(&self) -> DateTime<Utc> {
self.last_tick_time
}
pub fn total_net_value(&self) -> u64 {
self.balance
.saturating_add(self.running_margin())
.saturating_add_signed(self.running_pl())
}
pub fn balance(&self) -> u64 {
self.balance
}
pub fn market_price(&self) -> Price {
self.market_price
}
pub fn last_trade_time(&self) -> Option<DateTime<Utc>> {
self.last_trade_time
}
pub fn running_map(&self) -> &DynRunningTradesMap {
&self.running_map
}
pub fn running_long_len(&self) -> usize {
self.get_running_stats().long_len
}
pub fn running_long_margin(&self) -> u64 {
self.get_running_stats().long_margin
}
pub fn running_long_quantity(&self) -> u64 {
self.get_running_stats().long_quantity
}
pub fn running_short_len(&self) -> usize {
self.get_running_stats().short_len
}
pub fn running_short_margin(&self) -> u64 {
self.get_running_stats().short_margin
}
pub fn running_short_quantity(&self) -> u64 {
self.get_running_stats().short_quantity
}
pub fn running_margin(&self) -> u64 {
self.running_long_margin() + self.running_short_margin()
}
pub fn running_quantity(&self) -> u64 {
self.running_long_quantity() + self.running_short_quantity()
}
pub fn running_pl(&self) -> i64 {
self.get_running_stats().pl
}
pub fn running_fees(&self) -> u64 {
self.get_running_stats().fees
}
pub fn funding_fees(&self) -> i64 {
self.funding_fees
}
pub fn realized_pl(&self) -> i64 {
self.realized_pl
}
pub fn closed_history(&self) -> &Arc<ClosedTradeHistory> {
&self.closed_history
}
pub fn closed_len(&self) -> usize {
self.closed_history.len()
}
pub fn closed_fees(&self) -> u64 {
self.closed_fees
}
pub fn closed_net_pl(&self) -> i64 {
self.realized_pl - self.closed_fees() as i64
}
pub fn pl(&self) -> i64 {
self.running_pl() + self.realized_pl
}
pub fn fees(&self) -> u64 {
self.running_fees() + self.closed_fees()
}
pub fn summary(&self) -> String {
let mut result = String::new();
let last_trade_str = self
.last_trade_time()
.map_or("-".to_string(), |t| t.format_local_short());
let tick_str = self.last_tick_time().format_local_short();
let w = tick_str.len().max(last_trade_str.len());
result.push_str("Timestamps:\n");
result.push_str(&format!(" Tick: {:>w$}\n", tick_str));
result.push_str(&format!(" Last trade: {:>w$}\n\n", last_trade_str));
result.push_str(&format!("Price: {:.1} USD\n\n", self.market_price));
let price = self.market_price.as_f64();
let nav_sats = self.total_net_value().to_string();
let nav_usd = format!(
"{:.2}",
self.total_net_value() as f64 * price / SATS_PER_BTC
);
let w = nav_sats.len().max(nav_usd.len());
result.push_str(&format!("Net Asset Value: {:>w$} sats\n", nav_sats));
result.push_str(&format!(" {:>w$} USD\n\n", nav_usd));
let bal_sats = self.balance.to_string();
let bal_usd = format!("{:.2}", self.balance as f64 * price / SATS_PER_BTC);
let w = bal_sats.len().max(bal_usd.len());
result.push_str(&format!("Available balance: {:>w$} sats\n", bal_sats));
result.push_str(&format!(" {:>w$} USD\n\n", bal_usd));
let lt = self.running_long_len().to_string();
let lm = self.running_long_margin().to_string();
let lq = self.running_long_quantity().to_string();
let st = self.running_short_len().to_string();
let sm = self.running_short_margin().to_string();
let sq = self.running_short_quantity().to_string();
let w = [<, &lm, &lq, &st, &sm, &sq]
.iter()
.map(|s| s.len())
.max()
.unwrap_or(0);
result.push_str("Running Positions:\n");
result.push_str(" Long:\n");
result.push_str(&format!(" Trades: {:>w$}\n", lt));
result.push_str(&format!(" Margin: {:>w$} sats\n", lm));
result.push_str(&format!(" Quantity: {:>w$} USD\n", lq));
result.push_str(" Short:\n");
result.push_str(&format!(" Trades: {:>w$}\n", st));
result.push_str(&format!(" Margin: {:>w$} sats\n", sm));
result.push_str(&format!(" Quantity: {:>w$} USD\n\n", sq));
let rpl = self.running_pl().to_string();
let rf = self.running_fees().to_string();
let rm = self.running_margin().to_string();
let w = rpl.len().max(rf.len()).max(rm.len());
result.push_str("Running Metrics:\n");
result.push_str(&format!(" P/L: {:>w$} sats\n", rpl));
result.push_str(&format!(" Fees: {:>w$} sats\n", rf));
result.push_str(&format!(" Margin: {:>w$} sats\n\n", rm));
let ff = self.funding_fees.to_string();
let rp = self.realized_pl.to_string();
let w = ff.len().max(rp.len());
result.push_str(&format!("Funding fees: {:>w$} sats\n", ff));
result.push_str(&format!("Realized P/L: {:>w$} sats\n\n", rp));
let ct = self.closed_len().to_string();
let cf = self.closed_fees.to_string();
let w = ct.len().max(cf.len());
result.push_str("Closed:\n");
result.push_str(&format!(" Trades: {:>w$}\n", ct));
result.push_str(&format!(" Fees: {:>w$} sats", cf));
result
}
pub fn running_trades_table(&self) -> String {
if self.running_map.is_empty() {
return "No running trades.".to_string();
}
let mut table = String::new();
table.push_str(&format!(
"{:>14} | {:>5} | {:>11} | {:>11} | {:>11} | {:>11} | {:>5} | {:>11} | {:>8} | {:>11} | {:>11} | {:>11}",
"creation_time",
"side",
"quantity",
"price",
"liquidation",
"stoploss",
"TSL",
"takeprofit",
"leverage",
"margin",
"pl",
"fees"
));
table.push_str(&format!("\n{}", "-".repeat(153)));
for (trade, tsl) in self.running_map.trades_desc() {
let creation_time = trade
.created_at()
.with_timezone(&chrono::Local)
.format("%y-%m-%d %H:%M");
let stoploss_str = trade
.stoploss()
.map_or("N/A".to_string(), |sl| format!("{:.1}", sl));
let tsl_str = tsl.map_or("N/A".to_string(), |tsl| format!("{:.1}%", tsl.as_f64()));
let takeprofit_str = trade
.takeprofit()
.map_or("N/A".to_string(), |sl| format!("{:.1}", sl));
let total_margin = trade.margin().as_i64() + trade.maintenance_margin().max(0);
let pl = trade.est_pl(self.market_price).floor() as i64;
let total_fees = trade.opening_fee() + trade.closing_fee();
table.push_str(&format!(
"\n{:>14} | {:>5} | {:>11} | {:>11.1} | {:>11.1} | {:>11} | {:>5} | {:>11} | {:>8.2} | {:>11} | {:>11} | {:>11}",
creation_time,
trade.side(),
trade.quantity(),
trade.price(),
trade.liquidation(),
stoploss_str,
tsl_str,
takeprofit_str,
trade.leverage(),
total_margin,
pl,
total_fees
));
}
table
}
}
impl fmt::Display for TradingState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "TradingState:")?;
for line in self.summary().lines() {
write!(f, "\n {line}")?;
}
Ok(())
}
}
pub struct ClosedTradeHistory {
trades: BTreeMap<(DateTime<Utc>, Uuid), Arc<dyn TradeClosed>>,
id_to_time: HashMap<Uuid, DateTime<Utc>>,
}
impl ClosedTradeHistory {
pub fn new() -> Self {
Self {
trades: BTreeMap::new(),
id_to_time: HashMap::new(),
}
}
pub(super) fn add(&mut self, trade: Arc<dyn TradeClosed>) -> TradeCoreResult<()> {
if !trade.closed() || trade.exit_price().is_none() || trade.closed_at().is_none() {
return Err(TradeCoreError::TradeNotClosed {
trade_id: trade.id(),
});
}
let id = trade.id();
let created_at = trade.created_at();
self.trades.insert((created_at, id), trade);
self.id_to_time.insert(id, created_at);
Ok(())
}
pub fn get_by_id(&self, id: Uuid) -> Option<&Arc<dyn TradeClosed>> {
let creation_ts = self.id_to_time.get(&id)?;
self.trades.get(&(*creation_ts, id))
}
pub fn is_empty(&self) -> bool {
self.trades.is_empty()
}
pub fn len(&self) -> usize {
self.trades.len()
}
pub fn iter(&self) -> impl Iterator<Item = &Arc<dyn TradeClosed>> {
self.trades.values()
}
pub fn iter_desc(&self) -> impl Iterator<Item = &Arc<dyn TradeClosed>> {
self.trades.values().rev()
}
pub fn to_table(&self) -> String {
if self.trades.is_empty() {
return "No closed trades.".to_string();
}
let mut table = String::new();
table.push_str(&format!(
"{:>14} | {:>5} | {:>11} | {:>11} | {:>11} | {:>11} | {:>14} | {:>11} | {:>11} | {:>11}",
"creation_time",
"side",
"quantity",
"margin",
"price",
"exit_price",
"exit_time",
"pl",
"fees",
"net_pl"
));
table.push_str(&format!("\n{}", "-".repeat(137)));
for trade in self.trades.values().rev() {
let creation_time = trade
.created_at()
.with_timezone(&chrono::Local)
.format("%y-%m-%d %H:%M");
let exit_price = trade
.exit_price()
.expect("`closed` trade must have `exit_price`");
let exit_time = trade
.closed_at()
.expect("`closed` trade must have `closed_at`")
.with_timezone(&chrono::Local)
.format("%y-%m-%d %H:%M");
let pl = trade.pl();
let total_fees = trade.opening_fee() + trade.closing_fee();
let net_pl = pl - total_fees as i64;
table.push_str(&format!(
"\n{:>14} | {:>5} | {:>11} | {:>11} | {:>11} | {:>11} | {:>14} | {:>11} | {:>11} | {:>11}",
creation_time,
trade.side(),
trade.quantity(),
trade.margin(),
trade.price(),
exit_price,
exit_time,
pl,
total_fees,
net_pl
));
}
table
}
}
impl Clone for ClosedTradeHistory {
fn clone(&self) -> Self {
Self {
trades: self.trades.clone(),
id_to_time: self.id_to_time.clone(),
}
}
}
impl Default for ClosedTradeHistory {
fn default() -> Self {
Self::new()
}
}
impl fmt::Debug for ClosedTradeHistory {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ClosedTradeHistory")
.field("len", &self.trades.len())
.finish()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Stoploss {
Fixed(Price),
Trailing(PercentageCapped),
}
impl Stoploss {
pub(super) fn evaluate(
&self,
tsl_step_size: PercentageCapped,
side: TradeSide,
market_price: Price,
) -> TradeCoreResult<(Price, Option<TradeTrailingStoploss>)> {
match self {
Self::Fixed(price) => Ok((*price, None)),
Self::Trailing(tsl) => {
if tsl_step_size > *tsl {
return Err(TradeCoreError::InvalidStoplossSmallerThanTrailingStepSize {
tsl: *tsl,
tsl_step_size,
});
}
let initial_stoploss_price = match side {
TradeSide::Buy => market_price.apply_discount(*tsl).map_err(|e| {
TradeCoreError::InvalidPriceApplyDiscount {
price: market_price,
discount: *tsl,
e,
}
})?,
TradeSide::Sell => market_price.apply_gain((*tsl).into()).map_err(|e| {
TradeCoreError::InvalidPriceApplyGain {
price: market_price,
gain: (*tsl).into(),
e,
}
})?,
};
Ok((initial_stoploss_price, Some(TradeTrailingStoploss(*tsl))))
}
}
}
pub fn fixed(stoploss_price: Price) -> Self {
Self::Fixed(stoploss_price)
}
pub fn trailing(stoploss_perc: PercentageCapped) -> Self {
Self::Trailing(stoploss_perc)
}
}
impl From<Price> for Stoploss {
fn from(value: Price) -> Self {
Self::Fixed(value)
}
}
impl From<PercentageCapped> for Stoploss {
fn from(value: PercentageCapped) -> Self {
Self::Trailing(value)
}
}
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct TradeTrailingStoploss(PercentageCapped);
impl TradeTrailingStoploss {
pub(crate) fn prev_validated(tsl: PercentageCapped) -> Self {
Self(tsl)
}
pub fn as_f64(self) -> f64 {
self.0.as_f64()
}
}
impl From<TradeTrailingStoploss> for f64 {
fn from(value: TradeTrailingStoploss) -> Self {
value.0.as_f64()
}
}
impl From<TradeTrailingStoploss> for PercentageCapped {
fn from(value: TradeTrailingStoploss) -> Self {
value.0
}
}
impl From<TradeTrailingStoploss> for Percentage {
fn from(value: TradeTrailingStoploss) -> Self {
value.0.into()
}
}
#[async_trait]
pub trait TradeExecutor: Send + Sync {
async fn open_long(
&self,
size: TradeSize,
leverage: Leverage,
stoploss: Option<Stoploss>,
takeprofit: Option<Price>,
client_id: Option<ClientId>,
) -> TradeExecutorResult<Uuid>;
async fn open_short(
&self,
size: TradeSize,
leverage: Leverage,
stoploss: Option<Stoploss>,
takeprofit: Option<Price>,
client_id: Option<ClientId>,
) -> TradeExecutorResult<Uuid>;
async fn add_margin(&self, trade_id: Uuid, amount: NonZeroU64) -> TradeExecutorResult<()>;
async fn cash_in(&self, trade_id: Uuid, amount: NonZeroU64) -> TradeExecutorResult<()>;
async fn close_trade(&self, trade_id: Uuid) -> TradeExecutorResult<()>;
async fn close_longs(&self) -> TradeExecutorResult<Vec<Uuid>>;
async fn close_shorts(&self) -> TradeExecutorResult<Vec<Uuid>>;
async fn close_all(&self) -> TradeExecutorResult<Vec<Uuid>>;
async fn trading_state(&self) -> TradeExecutorResult<TradingState>;
}
#[async_trait]
pub trait SignalOperator<S: Signal>: Send + Sync {
fn set_trade_executor(&mut self, trade_executor: Arc<dyn TradeExecutor>) -> GeneralResult<()>;
async fn process_signal(&self, signal: &S) -> GeneralResult<()>;
}
pub(crate) struct WrappedSignalOperator<S: Signal>(Box<dyn SignalOperator<S>>);
impl<S: Signal> WrappedSignalOperator<S> {
pub fn set_trade_executor(
&mut self,
trade_executor: Arc<dyn TradeExecutor>,
) -> TradeCoreResult<()> {
panic::catch_unwind(AssertUnwindSafe(|| {
self.0.set_trade_executor(trade_executor)
}))
.map_err(|e| TradeCoreError::SignalOperatorSetTradeExecutorPanicked(e.into()))?
.map_err(|e| TradeCoreError::SignalOperatorSetTradeExecutorError(e.to_string()))
}
pub async fn process_signal(&self, signal: &S) -> TradeCoreResult<()> {
FutureExt::catch_unwind(AssertUnwindSafe(self.0.process_signal(signal)))
.await
.map_err(|e| TradeCoreError::SignalOperatorProcessSignalPanicked(e.into()))?
.map_err(|e| TradeCoreError::SignalOperatorProcessSignalError(e.to_string()))
}
}
impl<S: Signal> From<Box<dyn SignalOperator<S>>> for WrappedSignalOperator<S> {
fn from(value: Box<dyn SignalOperator<S>>) -> Self {
Self(value)
}
}
#[derive(Debug, Clone, Copy)]
pub struct Raw;
impl fmt::Display for Raw {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Raw")
}
}
#[async_trait]
pub trait RawOperator: Send + Sync {
fn set_trade_executor(&mut self, trade_executor: Arc<dyn TradeExecutor>) -> GeneralResult<()>;
fn lookback(&self) -> Option<Lookback>;
fn min_iteration_interval(&self) -> MinIterationInterval;
async fn iterate(&self, candles: &[OhlcCandleRow]) -> GeneralResult<()>;
}
pub(super) struct WrappedRawOperator(Box<dyn RawOperator>);
impl WrappedRawOperator {
pub fn set_trade_executor(
&mut self,
trade_executor: Arc<dyn TradeExecutor>,
) -> TradeCoreResult<()> {
panic::catch_unwind(AssertUnwindSafe(|| {
self.0.set_trade_executor(trade_executor)
}))
.map_err(|e| TradeCoreError::RawOperatorSetTradeExecutorPanicked(e.into()))?
.map_err(|e| TradeCoreError::RawOperatorSetTradeExecutorError(e.to_string()))
}
pub fn lookback(&self) -> TradeCoreResult<Option<Lookback>> {
let lookback = panic::catch_unwind(AssertUnwindSafe(|| self.0.lookback()))
.map_err(|e| TradeCoreError::RawOperatorLookbackPanicked(e.into()))?;
Ok(lookback)
}
pub fn min_iteration_interval(&self) -> TradeCoreResult<MinIterationInterval> {
let interval = panic::catch_unwind(AssertUnwindSafe(|| self.0.min_iteration_interval()))
.map_err(|e| TradeCoreError::RawOperatorMinIterationIntervalPanicked(e.into()))?;
Ok(interval)
}
pub async fn iterate(&self, candles: &[OhlcCandleRow]) -> TradeCoreResult<()> {
FutureExt::catch_unwind(AssertUnwindSafe(self.0.iterate(candles)))
.await
.map_err(|e| TradeCoreError::RawOperatorIteratePanicked(e.into()))?
.map_err(|e| TradeCoreError::RawOperatorIterateError(e.to_string()))
}
}
impl From<Box<dyn RawOperator>> for WrappedRawOperator {
fn from(value: Box<dyn RawOperator>) -> Self {
Self(value)
}
}
pub(super) trait TradeRunningExt: TradeRunning {
fn next_stoploss_update_trigger(
&self,
tsl_step_size: PercentageCapped,
trade_tsl: TradeTrailingStoploss,
) -> TradeCoreResult<Price> {
let tsl = trade_tsl.into();
if tsl_step_size > tsl {
return Err(TradeCoreError::InvalidStoplossSmallerThanTrailingStepSize {
tsl,
tsl_step_size,
});
}
let curr_stoploss =
self.stoploss()
.ok_or_else(|| TradeCoreError::NoNextTriggerTradeStoplossNotSet {
trade_id: self.id(),
})?;
let price_trigger = match self.side() {
TradeSide::Buy => {
let next_stoploss =
curr_stoploss
.apply_gain(tsl_step_size.into())
.map_err(|e| TradeCoreError::InvalidPriceApplyGain {
price: curr_stoploss,
gain: tsl_step_size.into(),
e,
})?;
let tsl_factor = 1.0 - trade_tsl.as_f64() / 100.0;
let trigger_price = next_stoploss.as_f64() / tsl_factor;
Price::round_up(trigger_price).map_err(|e| {
TradeCoreError::InvalidPriceRounding {
price: trigger_price,
e,
}
})?
}
TradeSide::Sell => {
let next_stoploss = curr_stoploss.apply_discount(tsl_step_size).map_err(|e| {
TradeCoreError::InvalidPriceApplyDiscount {
price: curr_stoploss,
discount: tsl_step_size,
e,
}
})?;
let tsl_factor = 1.0 + trade_tsl.as_f64() / 100.0;
let trigger_price = next_stoploss.as_f64() / tsl_factor;
Price::round_down(trigger_price).map_err(|e| {
TradeCoreError::InvalidPriceRounding {
price: trigger_price,
e,
}
})?
}
};
Ok(price_trigger)
}
fn eval_trigger_bounds(
&self,
tsl_step_size: PercentageCapped,
trade_tsl: Option<TradeTrailingStoploss>,
) -> TradeCoreResult<(Price, Price)> {
let next_stoploss_update_trigger = trade_tsl
.map(|tsl| self.next_stoploss_update_trigger(tsl_step_size, tsl))
.transpose()?;
match self.side() {
TradeSide::Buy => {
let lower_bound = self.stoploss().unwrap_or(self.liquidation());
let takeprofit_trigger = self.takeprofit().unwrap_or(Price::MAX);
let upper_bound =
takeprofit_trigger.min(next_stoploss_update_trigger.unwrap_or(Price::MAX));
Ok((lower_bound, upper_bound))
}
TradeSide::Sell => {
let takeprofit_trigger = self.takeprofit().unwrap_or(Price::MIN);
let lower_bound =
takeprofit_trigger.max(next_stoploss_update_trigger.unwrap_or(Price::MIN));
let upper_bound = self.stoploss().unwrap_or(self.liquidation());
Ok((lower_bound, upper_bound))
}
}
}
fn was_closed_on_range(&self, range_min: f64, range_max: f64) -> bool {
match self.side() {
TradeSide::Buy => {
let stoploss_reached = self
.stoploss()
.is_some_and(|stoploss| range_min <= stoploss.as_f64());
let liquidation_reached = range_min <= self.liquidation().as_f64();
let takeprofit_reached = self
.takeprofit()
.is_some_and(|takeprofit| range_max >= takeprofit.as_f64());
stoploss_reached || liquidation_reached || takeprofit_reached
}
TradeSide::Sell => {
let stoploss_reached = self
.stoploss()
.is_some_and(|stoploss| range_max >= stoploss.as_f64());
let liquidation_reached = range_max >= self.liquidation().as_f64();
let takeprofit_reached = self
.takeprofit()
.is_some_and(|takeprofit| range_min <= takeprofit.as_f64());
stoploss_reached || liquidation_reached || takeprofit_reached
}
}
}
fn eval_new_stoploss_on_range(
&self,
tsl_step_size: PercentageCapped,
trade_tsl: TradeTrailingStoploss,
range_min: f64,
range_max: f64,
) -> TradeCoreResult<Option<Price>> {
let next_stoploss_update_trigger = self
.next_stoploss_update_trigger(tsl_step_size, trade_tsl)?
.as_f64();
let new_stoploss = match self.side() {
TradeSide::Buy => {
if range_max >= next_stoploss_update_trigger {
let new_stoploss = Price::round(range_max).map_err(|e| {
TradeCoreError::InvalidPriceRounding {
price: range_max,
e,
}
})?;
let new_stoploss =
new_stoploss.apply_discount(trade_tsl.into()).map_err(|e| {
TradeCoreError::InvalidPriceApplyDiscount {
price: new_stoploss,
discount: trade_tsl.into(),
e,
}
})?;
Some(new_stoploss)
} else {
None
}
}
TradeSide::Sell => {
if range_min <= next_stoploss_update_trigger {
let new_stoploss = Price::round(range_min).map_err(|e| {
TradeCoreError::InvalidPriceRounding {
price: range_min,
e,
}
})?;
let new_stoploss = new_stoploss.apply_gain(trade_tsl.into()).map_err(|e| {
TradeCoreError::InvalidPriceApplyGain {
price: new_stoploss,
gain: trade_tsl.into(),
e,
}
})?;
Some(new_stoploss)
} else {
None
}
}
};
let new_stoploss = new_stoploss.filter(|new_sl| Some(*new_sl) != self.stoploss());
Ok(new_stoploss)
}
}
impl<T: TradeRunning + ?Sized> TradeRunningExt for T {}
#[derive(Debug, Clone)]
pub(super) enum PriceTrigger {
NotSet,
Set { min: Price, max: Price },
}
impl PriceTrigger {
pub fn new() -> Self {
Self::NotSet
}
pub fn update<T: TradeRunningExt + ?Sized>(
&mut self,
tsl_step_size: PercentageCapped,
trade: &T,
trade_tsl: Option<TradeTrailingStoploss>,
) -> TradeCoreResult<()> {
let (mut new_min, mut new_max) = trade.eval_trigger_bounds(tsl_step_size, trade_tsl)?;
if let PriceTrigger::Set { min, max } = *self {
new_min = new_min.max(min);
new_max = new_max.min(max);
}
*self = PriceTrigger::Set {
min: new_min,
max: new_max,
};
Ok(())
}
pub fn was_reached(&self, market_price: f64) -> bool {
match self {
PriceTrigger::NotSet => false,
PriceTrigger::Set { min, max } => {
market_price <= min.as_f64() || market_price >= max.as_f64()
}
}
}
}