use crate::framework::market::Market;
use crate::pocketoption::error::PocketResult;
use crate::pocketoption::types::Deal;
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tokio::sync::Mutex;
use uuid::Uuid;
#[derive(Debug, Serialize, Deserialize, Clone)]
struct VirtualTrade {
id: Uuid,
asset: String,
action: Action,
amount: Decimal,
entry_price: Decimal,
entry_time: i64,
duration: u32,
payout_percent: i32,
}
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
enum Action {
Call,
Put,
}
pub struct VirtualMarket {
balance: Mutex<Decimal>,
open_trades: Mutex<HashMap<Uuid, VirtualTrade>>,
current_prices: Mutex<HashMap<String, Decimal>>,
payouts: Mutex<HashMap<String, i32>>,
}
impl VirtualMarket {
pub fn new(initial_balance: Decimal) -> Self {
Self {
balance: Mutex::new(initial_balance),
open_trades: Mutex::new(HashMap::new()),
current_prices: Mutex::new(HashMap::new()),
payouts: Mutex::new(HashMap::new()),
}
}
pub async fn update_price(&self, asset: &str, price: Decimal) {
self.current_prices
.lock()
.await
.insert(asset.to_string(), price);
}
pub async fn set_payout(&self, asset: &str, payout: i32) {
self.payouts.lock().await.insert(asset.to_string(), payout);
}
}
#[async_trait]
impl Market for VirtualMarket {
async fn buy(&self, asset: &str, amount: Decimal, time: u32) -> PocketResult<(Uuid, Deal)> {
if amount <= dec!(0.0) {
return Err(crate::pocketoption::error::PocketError::General(
"Amount must be a positive number".into(),
));
}
let mut balance = self.balance.lock().await;
if *balance < amount {
return Err(crate::pocketoption::error::PocketError::General(
"Insufficient virtual balance".into(),
));
}
let entry_price = *self.current_prices.lock().await.get(asset).ok_or_else(|| {
crate::pocketoption::error::PocketError::General(format!(
"Price not found for asset: {}",
asset
))
})?;
let payout = *self.payouts.lock().await.get(asset).unwrap_or(&80);
*balance -= amount;
let id = Uuid::new_v4();
let entry_time = Utc::now();
let trade = VirtualTrade {
id,
asset: asset.to_string(),
action: Action::Call,
amount,
entry_price,
entry_time: entry_time.timestamp(),
duration: time,
payout_percent: payout,
};
self.open_trades.lock().await.insert(id, trade);
let deal = Deal {
id,
asset: asset.to_string(),
amount,
open_price: entry_price,
close_price: dec!(0.0),
open_timestamp: entry_time,
close_timestamp: entry_time + chrono::Duration::seconds(time as i64),
profit: dec!(0.0),
percent_profit: payout,
percent_loss: 100,
command: 0, uid: 0,
request_id: Some(id),
open_time: entry_time.to_rfc3339(),
close_time: (entry_time + chrono::Duration::seconds(time as i64)).to_rfc3339(),
refund_time: None,
refund_timestamp: None,
is_demo: 1,
copy_ticket: "".to_string(),
open_ms: 0,
close_ms: None,
option_type: 100,
is_rollover: None,
is_copy_signal: None,
is_ai: None,
currency: "USD".to_string(),
amount_usd: Some(amount),
amount_usd2: Some(amount),
};
Ok((id, deal))
}
async fn sell(&self, asset: &str, amount: Decimal, time: u32) -> PocketResult<(Uuid, Deal)> {
if amount <= dec!(0.0) {
return Err(crate::pocketoption::error::PocketError::General(
"Amount must be a positive number".into(),
));
}
let mut balance = self.balance.lock().await;
if *balance < amount {
return Err(crate::pocketoption::error::PocketError::General(
"Insufficient virtual balance".into(),
));
}
let entry_price = *self.current_prices.lock().await.get(asset).ok_or_else(|| {
crate::pocketoption::error::PocketError::General(format!(
"Price not found for asset: {}",
asset
))
})?;
let payout = *self.payouts.lock().await.get(asset).unwrap_or(&80);
*balance -= amount;
let id = Uuid::new_v4();
let entry_time = Utc::now();
let trade = VirtualTrade {
id,
asset: asset.to_string(),
action: Action::Put,
amount,
entry_price,
entry_time: entry_time.timestamp(),
duration: time,
payout_percent: payout,
};
self.open_trades.lock().await.insert(id, trade);
let deal = Deal {
id,
asset: asset.to_string(),
amount,
open_price: entry_price,
close_price: dec!(0.0),
open_timestamp: entry_time,
close_timestamp: entry_time + chrono::Duration::seconds(time as i64),
profit: dec!(0.0),
percent_profit: payout,
percent_loss: 100,
command: 1, uid: 0,
request_id: Some(id),
open_time: entry_time.to_rfc3339(),
close_time: (entry_time + chrono::Duration::seconds(time as i64)).to_rfc3339(),
refund_time: None,
refund_timestamp: None,
is_demo: 1,
copy_ticket: "".to_string(),
open_ms: 0,
close_ms: None,
option_type: 100,
is_rollover: None,
is_copy_signal: None,
is_ai: None,
currency: "USD".to_string(),
amount_usd: Some(amount),
amount_usd2: Some(amount),
};
Ok((id, deal))
}
async fn balance(&self) -> Decimal {
*self.balance.lock().await
}
async fn result(&self, trade_id: Uuid) -> PocketResult<Deal> {
let (trade, current_time, expiry_time) = {
let mut open_trades = self.open_trades.lock().await;
let trade = open_trades
.get(&trade_id)
.ok_or_else(|| {
crate::pocketoption::error::PocketError::General(format!(
"Trade {} not found",
trade_id
))
})?
.clone();
let current_time = Utc::now().timestamp();
let expiry_time = trade.entry_time + trade.duration as i64;
if current_time >= expiry_time {
open_trades.remove(&trade_id);
}
(trade, current_time, expiry_time)
};
let entry_timestamp = DateTime::from_timestamp(trade.entry_time, 0).unwrap_or_default();
let close_timestamp = DateTime::from_timestamp(expiry_time, 0).unwrap_or_default();
if current_time < expiry_time {
return Ok(Deal {
id: trade.id,
asset: trade.asset.clone(),
amount: trade.amount,
open_price: trade.entry_price,
close_price: dec!(0.0),
open_timestamp: entry_timestamp,
close_timestamp,
profit: dec!(0.0),
percent_profit: trade.payout_percent,
percent_loss: 100,
command: match trade.action {
Action::Call => 0,
Action::Put => 1,
},
uid: 0,
request_id: Some(trade.id),
open_time: entry_timestamp.to_rfc3339(),
close_time: close_timestamp.to_rfc3339(),
refund_time: None,
refund_timestamp: None,
is_demo: 1,
copy_ticket: "".to_string(),
open_ms: 0,
close_ms: None,
option_type: 100,
is_rollover: None,
is_copy_signal: None,
is_ai: None,
currency: "USD".to_string(),
amount_usd: Some(trade.amount),
amount_usd2: Some(trade.amount),
});
}
let mut balance = self.balance.lock().await;
let close_price = *self
.current_prices
.lock()
.await
.get(&trade.asset)
.ok_or_else(|| {
crate::pocketoption::error::PocketError::General(format!(
"Price not found for asset: {}",
trade.asset
))
})?;
let draw = close_price == trade.entry_price;
let win = !draw
&& match trade.action {
Action::Call => close_price > trade.entry_price,
Action::Put => close_price < trade.entry_price,
};
let profit = if win {
trade.amount * Decimal::from(trade.payout_percent) / dec!(100.0)
} else if draw {
dec!(0.0)
} else {
-trade.amount
};
let total_payout = trade.amount + profit;
if total_payout > dec!(0.0) {
*balance += total_payout;
}
let deal = Deal {
id: trade.id,
asset: trade.asset.clone(),
amount: trade.amount,
open_price: trade.entry_price,
close_price,
open_timestamp: entry_timestamp,
close_timestamp,
profit,
percent_profit: trade.payout_percent,
percent_loss: 100,
command: match trade.action {
Action::Call => 0,
Action::Put => 1,
},
uid: 0,
request_id: Some(trade.id),
open_time: entry_timestamp.to_rfc3339(),
close_time: close_timestamp.to_rfc3339(),
refund_time: None,
refund_timestamp: None,
is_demo: 1,
copy_ticket: "".to_string(),
open_ms: 0,
close_ms: None,
option_type: 100,
is_rollover: None,
is_copy_signal: None,
is_ai: None,
currency: "USD".to_string(),
amount_usd: Some(trade.amount),
amount_usd2: Some(trade.amount),
};
Ok(deal)
}
}