use std::fmt::{self, Display};
use chrono::{DateTime, Datelike as _, DurationRound as _, TimeDelta, TimeZone, Utc, Weekday};
use derive_getters::Getters;
use rust_decimal::Decimal;
use strum_macros::{Display, EnumIter};
use crate::{
core::{CoreError, CoreResult},
market::mexc_enums,
trade_log,
};
#[derive(Debug, thiserror::Error)]
#[error("Order error: {0}")]
pub struct OrderError(pub String);
impl OrderError {
pub fn new(msg: &str) -> Self {
OrderError(msg.to_string())
}
}
#[derive(Debug, Clone, Copy, Display, serde::Serialize, serde::Deserialize, PartialEq)]
#[serde(rename_all = "PascalCase")]
pub enum Crypto {
BTC,
DOGE,
ETH,
SOL,
USDC,
USDT,
XEL,
}
#[derive(Debug, Clone, Copy, Display, serde::Serialize, serde::Deserialize, PartialEq)]
#[serde(rename_all = "PascalCase")]
pub enum OrderStatus {
Open,
PartiallyFilled,
PartiallyCanceled,
Filled,
Canceled,
Tested,
Failed,
}
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, Display)]
pub enum Quantity {
Asset(Decimal), Quote(Decimal), }
impl Quantity {
pub fn get_amount(&self) -> Decimal {
match self {
Quantity::Asset(amount) => *amount,
Quantity::Quote(amount) => *amount,
}
}
pub fn get_amounts(&self) -> (Option<Decimal>, Option<Decimal>) {
match self {
Quantity::Asset(amount) => (Some(*amount), None),
Quantity::Quote(amount) => (None, Some(*amount)),
}
}
}
pub trait SpecificOrderDetails:
Clone
+ Default
+ fmt::Debug
+ Display
+ serde::Serialize
+ PartialEq
+ 'static
+ Send
+ Sync
+ for<'de> serde::Deserialize<'de>
{
const NAME: &'static str;
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, PartialEq)]
pub struct EmptySpecificOrderDetails {
content: Option<()>,
}
impl SpecificOrderDetails for EmptySpecificOrderDetails {
const NAME: &'static str = "EmptySpecificOrderDetails";
}
impl Display for EmptySpecificOrderDetails {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "<EmptySpecificOrderDetails>")
}
}
#[derive(Debug, Clone, Getters, serde::Serialize, serde::Deserialize, PartialEq)]
#[serde(bound = "D: SpecificOrderDetails")]
pub struct Order<D: SpecificOrderDetails> {
#[getter(skip)]
_id: String, asset: Crypto,
quote: Crypto,
side: mexc_enums::OrderSide,
order_type: mexc_enums::OrderType,
status: OrderStatus,
quantity: Quantity,
price: Option<Decimal>,
executed_qty: Decimal,
cummulative_quote_qty: Decimal,
details: D,
}
impl<D: SpecificOrderDetails> Order<D> {
pub fn new(
asset: Crypto,
quote: Crypto,
side: mexc_enums::OrderSide,
order_type: mexc_enums::OrderType,
quantity: Quantity,
price: Option<Decimal>,
) -> Result<Self, OrderError> {
match order_type {
mexc_enums::OrderType::Limit => {
if price.is_none() {
return Err(OrderError::new("Limit order should have a price"));
}
}
mexc_enums::OrderType::Market => {
if price.is_some() {
return Err(OrderError::new("Market order should not have a price"));
}
}
mexc_enums::OrderType::LimitMaker
| mexc_enums::OrderType::ImmediateOrCancel
| mexc_enums::OrderType::FillOrKill => {
return Err(OrderError::new("Unimplemented order type"))
}
}
if quantity.get_amount() <= Decimal::from(0) {
return Err(OrderError::new("Amount should be greater than 0"));
}
if price.is_some() && price.unwrap() <= Decimal::from(0) {
return Err(OrderError::new("Price should be greater than 0"));
}
tracing::trace!(
"Creating order: asset: {}, quote: {}, side: {:?}, type: {:?}, quantity: {}, price: {:?}",
asset,
quote,
side,
order_type,
quantity,
price);
Ok(Self {
asset,
quote,
side,
order_type,
quantity,
price,
status: OrderStatus::Open,
_id: uuid::Uuid::new_v4().to_string(),
executed_qty: Decimal::from(0),
cummulative_quote_qty: Decimal::from(0),
details: D::default(),
})
}
pub fn id(&self) -> &String {
&self._id
}
pub fn set_status(&mut self, status: OrderStatus) {
self.status = status;
trade_log!("Update on order {}: status now {}", self._id, status);
}
pub fn set_executed(&mut self, executed_qty: Decimal, cummulative_quote_qty: Decimal) {
self.executed_qty = executed_qty;
self.cummulative_quote_qty = cummulative_quote_qty;
tracing::debug!(
"Order {} executed: {} - {}",
self._id,
executed_qty,
cummulative_quote_qty
);
}
pub fn set_asset_quantity(&mut self, asset_quantity: Decimal) -> Result<(), OrderError> {
if self.order_type != mexc_enums::OrderType::Market
&& self.side != mexc_enums::OrderSide::Buy
{
return Err(OrderError::new(
"Asset quantity can only be modified for buy market orders",
));
}
self.quantity = Quantity::Asset(asset_quantity);
Ok(())
}
pub fn quote_quantity(&self) -> Result<Decimal, OrderError> {
match self.quantity {
Quantity::Asset(amount) => {
Ok(self.price.ok_or_else(|| OrderError::new("Price not set"))? * amount)
}
Quantity::Quote(amount) => Ok(amount),
}
}
pub fn is_done(&self) -> bool {
matches!(
self.status,
OrderStatus::Filled
| OrderStatus::Canceled
| OrderStatus::PartiallyCanceled
| OrderStatus::Tested
| OrderStatus::Failed
)
}
pub fn set_price(&mut self, price: Decimal) -> Result<(), OrderError> {
if self.order_type != mexc_enums::OrderType::Market && self.status != OrderStatus::Filled {
return Err(OrderError::new(
"Price can only be modified except for filled market orders",
));
}
self.price = Some(price);
Ok(())
}
pub fn symbol(&self) -> String {
format!("{}{}", self.asset(), self.quote())
}
pub fn details_mut(&mut self) -> &mut D {
&mut self.details
}
}
impl<D: SpecificOrderDetails> fmt::Display for Order<D> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"Buy: {}, With: {}, Side: {:?}, Type: {:?},Amount: {}, Price: {:?}, Market: {}, Status: {}, Specifc: {}",
self.asset, self.quote, self.side, self.order_type, self.quantity, self.price, D::NAME, self.status, self.details
)
}
}
#[derive(Debug, Clone, Copy)]
pub struct OrderDraft {
pub asset: Crypto,
pub quote: Crypto,
pub side: mexc_enums::OrderSide,
pub order_type: mexc_enums::OrderType,
pub amount: Quantity,
pub price: Option<Decimal>,
}
impl From<OrderStatus> for mexc_enums::OrderStatus {
fn from(status: OrderStatus) -> Self {
match status {
OrderStatus::Open => mexc_enums::OrderStatus::New,
OrderStatus::Filled => mexc_enums::OrderStatus::Filled,
OrderStatus::Canceled => mexc_enums::OrderStatus::Canceled,
OrderStatus::PartiallyFilled => mexc_enums::OrderStatus::PartiallyFilled,
OrderStatus::PartiallyCanceled => mexc_enums::OrderStatus::PartiallyCanceled,
OrderStatus::Tested => mexc_enums::OrderStatus::Canceled,
OrderStatus::Failed => mexc_enums::OrderStatus::Canceled,
}
}
}
impl From<mexc_enums::OrderStatus> for OrderStatus {
fn from(status: mexc_enums::OrderStatus) -> Self {
match status {
mexc_enums::OrderStatus::New => OrderStatus::Open,
mexc_enums::OrderStatus::Filled => OrderStatus::Filled,
mexc_enums::OrderStatus::Canceled => OrderStatus::Canceled,
mexc_enums::OrderStatus::PartiallyFilled => OrderStatus::PartiallyFilled,
mexc_enums::OrderStatus::PartiallyCanceled => OrderStatus::PartiallyCanceled,
}
}
}
#[derive(
Debug,
Display,
serde::Serialize,
serde::Deserialize,
Clone,
Copy,
PartialEq,
EnumIter,
Eq,
PartialOrd,
Ord,
)]
pub enum KlineInterval {
OneMinute,
FiveMinutes,
FifteenMinutes,
ThirtyMinutes,
OneHour,
FourHours,
OneDay,
OneWeek,
OneMonth,
}
impl KlineInterval {
pub fn next_interval_to_zoom_out(&self) -> Option<KlineInterval> {
match self {
KlineInterval::OneMinute => Some(KlineInterval::FiveMinutes),
KlineInterval::FiveMinutes => Some(KlineInterval::FifteenMinutes),
KlineInterval::FifteenMinutes => Some(KlineInterval::OneHour),
KlineInterval::ThirtyMinutes => Some(KlineInterval::OneHour),
KlineInterval::OneHour => Some(KlineInterval::OneDay),
KlineInterval::FourHours => Some(KlineInterval::OneDay),
KlineInterval::OneDay => Some(KlineInterval::OneWeek),
KlineInterval::OneWeek => None,
KlineInterval::OneMonth => None,
}
}
pub fn time_delta(&self) -> TimeDelta {
match self {
KlineInterval::OneMinute => TimeDelta::minutes(1),
KlineInterval::FiveMinutes => TimeDelta::minutes(5),
KlineInterval::FifteenMinutes => TimeDelta::minutes(15),
KlineInterval::ThirtyMinutes => TimeDelta::minutes(30),
KlineInterval::OneHour => TimeDelta::hours(1),
KlineInterval::FourHours => TimeDelta::hours(4),
KlineInterval::OneDay => TimeDelta::days(1),
KlineInterval::OneWeek => TimeDelta::weeks(1),
KlineInterval::OneMonth => TimeDelta::days(30),
}
}
pub fn get_time_bounds(
&self,
target: DateTime<Utc>,
) -> CoreResult<(DateTime<Utc>, DateTime<Utc>)> {
match self {
KlineInterval::OneWeek => {
let mut start = target.duration_trunc(KlineInterval::OneDay.time_delta())?;
let mut end = target.duration_round_up(KlineInterval::OneDay.time_delta())?;
while start.weekday() != Weekday::Mon {
start -= TimeDelta::days(1);
}
while end.weekday() != Weekday::Mon && end <= start {
end += TimeDelta::days(1);
}
Ok((end, start))
}
KlineInterval::OneMonth => {
let start = Utc
.with_ymd_and_hms(target.year(), target.month(), 1, 0, 0, 0)
.single()
.ok_or(CoreError::comput_error("Error creating start date"))?;
let end = if target.month() == 12 {
Utc.with_ymd_and_hms(target.year() + 1, 1, 1, 0, 0, 0)
} else {
Utc.with_ymd_and_hms(target.year(), target.month() + 1, 1, 0, 0, 0)
}
.single()
.ok_or(CoreError::comput_error("Error creating start date"))?;
Ok((start, end))
}
_ => {
let start = target.duration_trunc(self.time_delta())?;
let end = target.duration_round_up(self.time_delta())?;
if start != end {
Ok((start, end))
} else {
Ok((start, end + self.time_delta()))
}
}
}
}
pub fn short_string(&self) -> String {
match self {
KlineInterval::OneMinute => "1m",
KlineInterval::FiveMinutes => "5m",
KlineInterval::FifteenMinutes => "15m",
KlineInterval::ThirtyMinutes => "30m",
KlineInterval::OneHour => "1h",
KlineInterval::FourHours => "4h",
KlineInterval::OneDay => "1d",
KlineInterval::OneWeek => "1w",
KlineInterval::OneMonth => "1M",
}
.to_string()
}
}