use chrono::{DateTime, Utc};
pub type Result<T> = std::result::Result<T, Error>;
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Candle data is empty: backtesting requires at least one candle")]
CandleDataEmpty,
#[error("Candle not found")]
CandleNotFound,
#[error("The Aggregator factor is invalid")]
InvalidFactor,
#[error("Missing required field: {0}")]
MissingField(&'static str),
#[error("Invalid price order: open={0}, low={1}, high={2}, close={3}")]
InvalidPriceOrder(f64, f64, f64, f64),
#[error("Volume cannot be negative (got: {0})")]
NegativeVolume(f64),
#[error("Invalid time order: open={0}, close={1}")]
InvalideTimes(DateTime<Utc>, DateTime<Utc>),
#[error("Balance must be positive (got: {0})")]
NegZeroBalance(f64),
#[error("Insufficient funds: required {0}, available {1}")]
InsufficientFunds(f64, f64),
#[error("Negative free balance: balance={0}, locked={1}")]
NegFreeBalance(f64, f64),
#[error("Negative fees")]
NegZeroFees,
#[error("Locked funds {0} are insufficient for amount {1}")]
UnlockBalance(f64, f64),
#[error("Order not found")]
OrderNotFound,
#[error("Failed to remove order")]
RemoveOrder,
#[error("Position not found")]
PositionNotFound,
#[error("Failed to remove position")]
RemovePosition,
#[error("Invalid exit price {0}")]
ExitPrice(f64),
#[error("{0}")]
Msg(String),
#[error("TakeProfit or StopLoss must be positive")]
NegTakeProfitAndStopLoss,
#[error("TrailingStop must be positive and greater than 0")]
NegZeroTrailingStop,
#[error("Try another order type")]
MismatchedOrderType,
#[cfg(feature = "draws")]
#[error("{0}")]
Plotters(String),
#[cfg(feature = "draws")]
#[error("{0}")]
Charming(#[from] charming::EchartsError),
}
#[cfg(feature = "serde")]
impl serde::Serialize for Error {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.to_string().as_ref())
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for Error {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(serde::Deserialize)]
#[serde(tag = "type", content = "data")]
enum ErrorWrapper {
CandleDataEmpty,
CandleNotFound,
InvalidFactor,
MissingField {
field: String,
},
InvalidPriceOrder {
open: f64,
low: f64,
high: f64,
close: f64,
},
NegativeVolume {
volume: f64,
},
InvalideTimes {
open: i64,
close: i64,
},
NegZeroBalance {
balance: f64,
},
InsufficientFunds {
required: f64,
available: f64,
},
NegFreeBalance {
balance: f64,
locked: f64,
},
NegZeroFees,
UnlockBalance {
locked: f64,
amount: f64,
},
OrderNotFound,
RemoveOrder,
PositionNotFound,
RemovePosition,
ExitPrice {
price: f64,
},
Msg {
message: String,
},
NegTakeProfitAndStopLoss,
NegZeroTrailingStop,
MismatchedOrderType,
#[cfg(feature = "draws")]
Plotters {
error: String,
},
#[cfg(feature = "draws")]
Charming {
error: String,
},
}
let wrapper = ErrorWrapper::deserialize(deserializer)?;
Ok(match wrapper {
ErrorWrapper::CandleDataEmpty => Error::CandleDataEmpty,
ErrorWrapper::CandleNotFound => Error::CandleNotFound,
ErrorWrapper::InvalidFactor => Error::InvalidFactor,
ErrorWrapper::MissingField { field } => Error::MissingField(Box::leak(field.into_boxed_str())),
ErrorWrapper::InvalidPriceOrder { open, low, high, close } => {
Error::InvalidPriceOrder(open, low, high, close)
}
ErrorWrapper::NegativeVolume { volume } => Error::NegativeVolume(volume),
ErrorWrapper::InvalideTimes { open, close } => {
let open_dt = DateTime::from_timestamp_millis(open).unwrap_or(Utc::now());
let close_dt = DateTime::from_timestamp_millis(close).unwrap_or(Utc::now());
Error::InvalideTimes(open_dt, close_dt)
}
ErrorWrapper::NegZeroBalance { balance } => Error::NegZeroBalance(balance),
ErrorWrapper::InsufficientFunds { required, available } => Error::InsufficientFunds(required, available),
ErrorWrapper::NegFreeBalance { balance, locked } => Error::NegFreeBalance(balance, locked),
ErrorWrapper::NegZeroFees => Error::NegZeroFees,
ErrorWrapper::UnlockBalance { locked, amount } => Error::UnlockBalance(locked, amount),
ErrorWrapper::OrderNotFound => Error::OrderNotFound,
ErrorWrapper::RemoveOrder => Error::RemoveOrder,
ErrorWrapper::PositionNotFound => Error::PositionNotFound,
ErrorWrapper::RemovePosition => Error::RemovePosition,
ErrorWrapper::ExitPrice { price } => Error::ExitPrice(price),
ErrorWrapper::Msg { message } => Error::Msg(message),
ErrorWrapper::NegTakeProfitAndStopLoss => Error::NegTakeProfitAndStopLoss,
ErrorWrapper::NegZeroTrailingStop => Error::NegZeroTrailingStop,
ErrorWrapper::MismatchedOrderType => Error::MismatchedOrderType,
#[cfg(feature = "draws")]
ErrorWrapper::Plotters { error } => Error::Plotters(error),
#[cfg(feature = "draws")]
ErrorWrapper::Charming { error } => Error::Charming(charming::EchartsError::HtmlRenderingError(error)),
})
}
}