use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use std::fmt;
use uuid::Uuid;
use super::user::ValidationError;
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct AdvancedOrder {
pub order_id: Uuid,
pub user_id: Uuid,
pub token_id: Uuid,
pub amount: Decimal,
pub order_type: AdvancedOrderType,
pub status: AdvancedOrderStatus,
pub trigger_price: Option<Decimal>,
pub limit_price: Option<Decimal>,
pub trailing_distance: Option<Decimal>,
pub current_trailing_price: Option<Decimal>,
pub expires_at: Option<DateTime<Utc>>,
pub twap_start_time: Option<DateTime<Utc>>,
pub twap_end_time: Option<DateTime<Utc>>,
pub twap_interval_seconds: Option<i64>,
pub twap_amount_per_interval: Option<Decimal>,
pub twap_executed_intervals: i32,
pub filled_amount: Decimal,
pub average_fill_price: Option<Decimal>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, sqlx::Type, PartialEq, Eq)]
#[sqlx(type_name = "varchar", rename_all = "snake_case")]
#[derive(Default)]
pub enum AdvancedOrderType {
StopLoss,
TakeProfit,
TrailingStop,
Twap,
#[default]
GoodTillTime,
}
impl fmt::Display for AdvancedOrderType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AdvancedOrderType::StopLoss => write!(f, "stop_loss"),
AdvancedOrderType::TakeProfit => write!(f, "take_profit"),
AdvancedOrderType::TrailingStop => write!(f, "trailing_stop"),
AdvancedOrderType::Twap => write!(f, "twap"),
AdvancedOrderType::GoodTillTime => write!(f, "good_till_time"),
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, sqlx::Type, PartialEq, Eq)]
#[sqlx(type_name = "varchar", rename_all = "lowercase")]
#[derive(Default)]
pub enum AdvancedOrderStatus {
#[default]
Active,
Triggered,
PartiallyFilled,
Filled,
Cancelled,
Expired,
Failed,
}
impl fmt::Display for AdvancedOrderStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AdvancedOrderStatus::Active => write!(f, "active"),
AdvancedOrderStatus::Triggered => write!(f, "triggered"),
AdvancedOrderStatus::PartiallyFilled => write!(f, "partially_filled"),
AdvancedOrderStatus::Filled => write!(f, "filled"),
AdvancedOrderStatus::Cancelled => write!(f, "cancelled"),
AdvancedOrderStatus::Expired => write!(f, "expired"),
AdvancedOrderStatus::Failed => write!(f, "failed"),
}
}
}
impl AdvancedOrder {
pub fn should_trigger(&self, current_price: Decimal) -> bool {
if self.status != AdvancedOrderStatus::Active {
return false;
}
match self.order_type {
AdvancedOrderType::StopLoss => {
if let Some(trigger) = self.trigger_price {
current_price <= trigger
} else {
false
}
}
AdvancedOrderType::TakeProfit => {
if let Some(trigger) = self.trigger_price {
current_price >= trigger
} else {
false
}
}
AdvancedOrderType::TrailingStop => {
if let Some(trailing) = self.current_trailing_price {
current_price <= trailing
} else {
false
}
}
AdvancedOrderType::Twap => {
self.should_execute_twap_interval()
}
AdvancedOrderType::GoodTillTime => {
if let Some(limit) = self.limit_price {
current_price <= limit } else {
true
}
}
}
}
pub fn update_trailing_stop(&mut self, current_price: Decimal) -> bool {
if self.order_type != AdvancedOrderType::TrailingStop {
return false;
}
let trailing_distance = match self.trailing_distance {
Some(d) => d,
None => return false,
};
let new_trailing_price = current_price - trailing_distance;
if let Some(current_trailing) = self.current_trailing_price {
if new_trailing_price > current_trailing {
self.current_trailing_price = Some(new_trailing_price);
self.updated_at = Utc::now();
return true;
}
} else {
self.current_trailing_price = Some(new_trailing_price);
self.updated_at = Utc::now();
return true;
}
false
}
pub fn is_expired(&self) -> bool {
if let Some(expires_at) = self.expires_at {
Utc::now() > expires_at
} else {
false
}
}
fn should_execute_twap_interval(&self) -> bool {
if self.order_type != AdvancedOrderType::Twap {
return false;
}
let now = Utc::now();
if let (Some(start), Some(end)) = (self.twap_start_time, self.twap_end_time) {
if now < start || now > end {
return false;
}
if let Some(interval_seconds) = self.twap_interval_seconds {
let elapsed = (now - start).num_seconds();
let expected_intervals = (elapsed / interval_seconds) as i32;
return expected_intervals > self.twap_executed_intervals;
}
}
false
}
pub fn remaining_twap_amount(&self) -> Decimal {
self.amount - self.filled_amount
}
pub fn next_twap_execution_amount(&self) -> Option<Decimal> {
if self.order_type != AdvancedOrderType::Twap {
return None;
}
let per_interval = self.twap_amount_per_interval?;
let remaining = self.remaining_twap_amount();
Some(per_interval.min(remaining))
}
pub fn validate(&self) -> Result<(), ValidationError> {
if self.amount <= dec!(0) {
return Err(ValidationError("Amount must be positive".to_string()));
}
match self.order_type {
AdvancedOrderType::StopLoss | AdvancedOrderType::TakeProfit => {
if self.trigger_price.is_none() {
return Err(ValidationError(
"Trigger price required for stop/take-profit orders".to_string(),
));
}
if let Some(trigger) = self.trigger_price {
if trigger <= dec!(0) {
return Err(ValidationError(
"Trigger price must be positive".to_string(),
));
}
}
}
AdvancedOrderType::TrailingStop => {
if self.trailing_distance.is_none() {
return Err(ValidationError(
"Trailing distance required for trailing stop".to_string(),
));
}
if let Some(distance) = self.trailing_distance {
if distance <= dec!(0) {
return Err(ValidationError(
"Trailing distance must be positive".to_string(),
));
}
}
}
AdvancedOrderType::Twap => {
if self.twap_start_time.is_none()
|| self.twap_end_time.is_none()
|| self.twap_interval_seconds.is_none()
{
return Err(ValidationError(
"TWAP orders require start time, end time, and interval".to_string(),
));
}
if let (Some(start), Some(end)) = (self.twap_start_time, self.twap_end_time) {
if end <= start {
return Err(ValidationError(
"TWAP end time must be after start time".to_string(),
));
}
}
if let Some(interval) = self.twap_interval_seconds {
if interval <= 0 {
return Err(ValidationError(
"TWAP interval must be positive".to_string(),
));
}
}
}
AdvancedOrderType::GoodTillTime => {
if self.expires_at.is_none() {
return Err(ValidationError(
"GTT orders require expiration time".to_string(),
));
}
}
}
Ok(())
}
}
impl fmt::Display for AdvancedOrder {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"AdvancedOrder({}, type={}, amount={}, status={})",
self.order_id, self.order_type, self.amount, self.status
)
}
}
#[derive(Debug, Deserialize)]
pub struct CreateStopLossRequest {
pub token_id: Uuid,
pub amount: Decimal,
pub trigger_price: Decimal,
pub limit_price: Option<Decimal>,
}
impl CreateStopLossRequest {
pub fn validate(&self) -> Result<(), ValidationError> {
if self.amount <= dec!(0) {
return Err(ValidationError("Amount must be positive".to_string()));
}
if self.trigger_price <= dec!(0) {
return Err(ValidationError(
"Trigger price must be positive".to_string(),
));
}
if let Some(limit) = self.limit_price {
if limit <= dec!(0) {
return Err(ValidationError("Limit price must be positive".to_string()));
}
if limit > self.trigger_price {
return Err(ValidationError(
"Limit price should be below trigger for stop-loss".to_string(),
));
}
}
Ok(())
}
}
#[derive(Debug, Deserialize)]
pub struct CreateTakeProfitRequest {
pub token_id: Uuid,
pub amount: Decimal,
pub trigger_price: Decimal,
pub limit_price: Option<Decimal>,
}
impl CreateTakeProfitRequest {
pub fn validate(&self) -> Result<(), ValidationError> {
if self.amount <= dec!(0) {
return Err(ValidationError("Amount must be positive".to_string()));
}
if self.trigger_price <= dec!(0) {
return Err(ValidationError(
"Trigger price must be positive".to_string(),
));
}
if let Some(limit) = self.limit_price {
if limit <= dec!(0) {
return Err(ValidationError("Limit price must be positive".to_string()));
}
if limit < self.trigger_price {
return Err(ValidationError(
"Limit price should be above trigger for take-profit".to_string(),
));
}
}
Ok(())
}
}
#[derive(Debug, Deserialize)]
pub struct CreateTrailingStopRequest {
pub token_id: Uuid,
pub amount: Decimal,
pub trailing_distance: Decimal,
pub initial_price: Decimal,
}
impl CreateTrailingStopRequest {
pub fn validate(&self) -> Result<(), ValidationError> {
if self.amount <= dec!(0) {
return Err(ValidationError("Amount must be positive".to_string()));
}
if self.trailing_distance <= dec!(0) {
return Err(ValidationError(
"Trailing distance must be positive".to_string(),
));
}
if self.initial_price <= dec!(0) {
return Err(ValidationError(
"Initial price must be positive".to_string(),
));
}
if self.trailing_distance >= self.initial_price {
return Err(ValidationError(
"Trailing distance must be less than initial price".to_string(),
));
}
Ok(())
}
}
#[derive(Debug, Deserialize)]
pub struct CreateTwapOrderRequest {
pub token_id: Uuid,
pub total_amount: Decimal,
pub start_time: DateTime<Utc>,
pub end_time: DateTime<Utc>,
pub interval_seconds: i64,
}
impl CreateTwapOrderRequest {
pub fn validate(&self) -> Result<(), ValidationError> {
if self.total_amount <= dec!(0) {
return Err(ValidationError("Total amount must be positive".to_string()));
}
if self.end_time <= self.start_time {
return Err(ValidationError(
"End time must be after start time".to_string(),
));
}
if self.interval_seconds <= 0 {
return Err(ValidationError("Interval must be positive".to_string()));
}
let duration_seconds = (self.end_time - self.start_time).num_seconds();
if self.interval_seconds > duration_seconds {
return Err(ValidationError(
"Interval cannot be longer than total duration".to_string(),
));
}
Ok(())
}
pub fn amount_per_interval(&self) -> Decimal {
let duration_seconds = (self.end_time - self.start_time).num_seconds();
let num_intervals = duration_seconds / self.interval_seconds;
if num_intervals > 0 {
self.total_amount / Decimal::from(num_intervals)
} else {
self.total_amount
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_stop_loss_trigger() {
let order = AdvancedOrder {
order_id: Uuid::new_v4(),
user_id: Uuid::new_v4(),
token_id: Uuid::new_v4(),
amount: dec!(100),
order_type: AdvancedOrderType::StopLoss,
status: AdvancedOrderStatus::Active,
trigger_price: Some(dec!(50)),
limit_price: None,
trailing_distance: None,
current_trailing_price: None,
expires_at: None,
twap_start_time: None,
twap_end_time: None,
twap_interval_seconds: None,
twap_amount_per_interval: None,
twap_executed_intervals: 0,
filled_amount: dec!(0),
average_fill_price: None,
created_at: Utc::now(),
updated_at: Utc::now(),
};
assert!(order.should_trigger(dec!(49)));
assert!(order.should_trigger(dec!(50)));
assert!(!order.should_trigger(dec!(51)));
}
#[test]
fn test_take_profit_trigger() {
let order = AdvancedOrder {
order_id: Uuid::new_v4(),
user_id: Uuid::new_v4(),
token_id: Uuid::new_v4(),
amount: dec!(100),
order_type: AdvancedOrderType::TakeProfit,
status: AdvancedOrderStatus::Active,
trigger_price: Some(dec!(100)),
limit_price: None,
trailing_distance: None,
current_trailing_price: None,
expires_at: None,
twap_start_time: None,
twap_end_time: None,
twap_interval_seconds: None,
twap_amount_per_interval: None,
twap_executed_intervals: 0,
filled_amount: dec!(0),
average_fill_price: None,
created_at: Utc::now(),
updated_at: Utc::now(),
};
assert!(order.should_trigger(dec!(101)));
assert!(order.should_trigger(dec!(100)));
assert!(!order.should_trigger(dec!(99)));
}
#[test]
fn test_trailing_stop_update() {
let mut order = AdvancedOrder {
order_id: Uuid::new_v4(),
user_id: Uuid::new_v4(),
token_id: Uuid::new_v4(),
amount: dec!(100),
order_type: AdvancedOrderType::TrailingStop,
status: AdvancedOrderStatus::Active,
trigger_price: None,
limit_price: None,
trailing_distance: Some(dec!(5)),
current_trailing_price: Some(dec!(95)),
expires_at: None,
twap_start_time: None,
twap_end_time: None,
twap_interval_seconds: None,
twap_amount_per_interval: None,
twap_executed_intervals: 0,
filled_amount: dec!(0),
average_fill_price: None,
created_at: Utc::now(),
updated_at: Utc::now(),
};
assert!(order.update_trailing_stop(dec!(105)));
assert_eq!(order.current_trailing_price, Some(dec!(100)));
assert!(!order.update_trailing_stop(dec!(103)));
assert_eq!(order.current_trailing_price, Some(dec!(100)));
}
#[test]
fn test_order_expiration() {
let past_time = Utc::now() - chrono::Duration::hours(1);
let future_time = Utc::now() + chrono::Duration::hours(1);
let expired_order = AdvancedOrder {
order_id: Uuid::new_v4(),
user_id: Uuid::new_v4(),
token_id: Uuid::new_v4(),
amount: dec!(100),
order_type: AdvancedOrderType::GoodTillTime,
status: AdvancedOrderStatus::Active,
trigger_price: None,
limit_price: Some(dec!(50)),
trailing_distance: None,
current_trailing_price: None,
expires_at: Some(past_time),
twap_start_time: None,
twap_end_time: None,
twap_interval_seconds: None,
twap_amount_per_interval: None,
twap_executed_intervals: 0,
filled_amount: dec!(0),
average_fill_price: None,
created_at: Utc::now(),
updated_at: Utc::now(),
};
assert!(expired_order.is_expired());
let active_order = AdvancedOrder {
expires_at: Some(future_time),
..expired_order
};
assert!(!active_order.is_expired());
}
#[test]
fn test_twap_amount_calculation() {
let now = Utc::now();
let request = CreateTwapOrderRequest {
token_id: Uuid::new_v4(),
total_amount: dec!(1000),
start_time: now,
end_time: now + chrono::Duration::hours(10),
interval_seconds: 3600, };
let amount_per_interval = request.amount_per_interval();
assert_eq!(amount_per_interval, dec!(100));
}
}