use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TraderProfile {
pub trader_id: Uuid,
pub username: String,
pub follower_count: u32,
pub win_rate: Decimal,
pub total_pnl: Decimal,
pub avg_monthly_return: Decimal,
pub max_drawdown: Decimal,
pub total_trades: u32,
pub commission_rate: Decimal,
pub accepting_followers: bool,
pub max_follower_exposure: Decimal,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CopyRelationship {
pub id: Uuid,
pub follower_id: Uuid,
pub leader_id: Uuid,
pub copy_percentage: Decimal,
pub max_exposure: Decimal,
pub current_exposure: Decimal,
pub started_at: DateTime<Utc>,
pub active: bool,
pub total_pnl: Decimal,
}
impl CopyRelationship {
pub fn new(
follower_id: Uuid,
leader_id: Uuid,
copy_percentage: Decimal,
max_exposure: Decimal,
) -> Self {
Self {
id: Uuid::new_v4(),
follower_id,
leader_id,
copy_percentage,
max_exposure,
current_exposure: Decimal::ZERO,
started_at: Utc::now(),
active: true,
total_pnl: Decimal::ZERO,
}
}
pub fn can_copy(&self, trade_amount: Decimal) -> bool {
self.active && (self.current_exposure + trade_amount) <= self.max_exposure
}
pub fn calculate_copy_amount(
&self,
leader_trade_amount: Decimal,
follower_balance: Decimal,
) -> Decimal {
let max_by_percentage = follower_balance * self.copy_percentage;
let max_by_exposure = self.max_exposure - self.current_exposure;
let scaled_amount = leader_trade_amount * self.copy_percentage;
scaled_amount.min(max_by_percentage).min(max_by_exposure)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CopyTrade {
pub id: Uuid,
pub leader_trade_id: Uuid,
pub follower_trade_id: Uuid,
pub relationship_id: Uuid,
pub leader_id: Uuid,
pub follower_id: Uuid,
pub token_id: Uuid,
pub leader_amount: Decimal,
pub follower_amount: Decimal,
pub price: Decimal,
pub commission: Decimal,
pub executed_at: DateTime<Utc>,
}
pub struct SocialTradingManager {
profiles: HashMap<Uuid, TraderProfile>,
relationships: HashMap<Uuid, CopyRelationship>,
follower_leaders: HashMap<Uuid, Vec<Uuid>>,
leader_followers: HashMap<Uuid, Vec<Uuid>>,
}
impl SocialTradingManager {
pub fn new() -> Self {
Self {
profiles: HashMap::new(),
relationships: HashMap::new(),
follower_leaders: HashMap::new(),
leader_followers: HashMap::new(),
}
}
pub fn add_profile(&mut self, profile: TraderProfile) {
self.profiles.insert(profile.trader_id, profile);
}
pub fn follow(
&mut self,
follower_id: Uuid,
leader_id: Uuid,
copy_percentage: Decimal,
max_exposure: Decimal,
) -> Result<Uuid, String> {
let leader = self.profiles.get(&leader_id).ok_or("Leader not found")?;
if !leader.accepting_followers {
return Err("Leader not accepting followers".to_string());
}
let relationship =
CopyRelationship::new(follower_id, leader_id, copy_percentage, max_exposure);
let relationship_id = relationship.id;
self.relationships.insert(relationship_id, relationship);
self.follower_leaders
.entry(follower_id)
.or_default()
.push(leader_id);
self.leader_followers
.entry(leader_id)
.or_default()
.push(follower_id);
Ok(relationship_id)
}
pub fn unfollow(&mut self, relationship_id: Uuid) -> Result<(), String> {
if let Some(relationship) = self.relationships.get_mut(&relationship_id) {
relationship.active = false;
Ok(())
} else {
Err("Relationship not found".to_string())
}
}
pub fn get_copy_trades(
&self,
leader_id: Uuid,
leader_trade_amount: Decimal,
follower_balances: &HashMap<Uuid, Decimal>,
) -> Vec<(Uuid, Decimal)> {
let followers = match self.leader_followers.get(&leader_id) {
Some(f) => f,
None => return Vec::new(),
};
let mut copy_trades = Vec::new();
for follower_id in followers {
if let Some(relationship) = self.find_relationship(*follower_id, leader_id) {
if let Some(&balance) = follower_balances.get(follower_id) {
let copy_amount =
relationship.calculate_copy_amount(leader_trade_amount, balance);
if copy_amount > Decimal::ZERO && relationship.can_copy(copy_amount) {
copy_trades.push((*follower_id, copy_amount));
}
}
}
}
copy_trades
}
fn find_relationship(&self, follower_id: Uuid, leader_id: Uuid) -> Option<&CopyRelationship> {
self.relationships
.values()
.find(|r| r.follower_id == follower_id && r.leader_id == leader_id && r.active)
}
pub fn calculate_commission(&self, leader_id: Uuid, follower_profit: Decimal) -> Decimal {
if let Some(profile) = self.profiles.get(&leader_id) {
if follower_profit > Decimal::ZERO {
return follower_profit * profile.commission_rate;
}
}
Decimal::ZERO
}
pub fn get_top_traders(&self, limit: usize) -> Vec<&TraderProfile> {
let mut profiles: Vec<&TraderProfile> = self.profiles.values().collect();
profiles.sort_by(|a, b| {
b.avg_monthly_return
.partial_cmp(&a.avg_monthly_return)
.unwrap()
});
profiles.into_iter().take(limit).collect()
}
pub fn get_follower_count(&self, leader_id: Uuid) -> usize {
self.leader_followers
.get(&leader_id)
.map(|f| f.len())
.unwrap_or(0)
}
}
impl Default for SocialTradingManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
#[test]
fn test_copy_relationship_creation() {
let follower_id = Uuid::new_v4();
let leader_id = Uuid::new_v4();
let relationship = CopyRelationship::new(follower_id, leader_id, dec!(0.5), dec!(10000));
assert_eq!(relationship.follower_id, follower_id);
assert_eq!(relationship.leader_id, leader_id);
assert_eq!(relationship.copy_percentage, dec!(0.5));
assert!(relationship.active);
}
#[test]
fn test_can_copy() {
let relationship =
CopyRelationship::new(Uuid::new_v4(), Uuid::new_v4(), dec!(0.5), dec!(1000));
assert!(relationship.can_copy(dec!(500)));
assert!(relationship.can_copy(dec!(1000)));
assert!(!relationship.can_copy(dec!(1001)));
}
#[test]
fn test_calculate_copy_amount() {
let relationship =
CopyRelationship::new(Uuid::new_v4(), Uuid::new_v4(), dec!(0.5), dec!(1000));
let copy_amount = relationship.calculate_copy_amount(dec!(1000), dec!(2000));
assert_eq!(copy_amount, dec!(500));
}
#[test]
fn test_follow_trader() {
let mut manager = SocialTradingManager::new();
let leader_id = Uuid::new_v4();
let leader_profile = TraderProfile {
trader_id: leader_id,
username: "Leader".to_string(),
follower_count: 0,
win_rate: dec!(0.75),
total_pnl: dec!(10000),
avg_monthly_return: dec!(0.15),
max_drawdown: dec!(-500),
total_trades: 100,
commission_rate: dec!(0.10),
accepting_followers: true,
max_follower_exposure: dec!(50000),
};
manager.add_profile(leader_profile);
let follower_id = Uuid::new_v4();
let result = manager.follow(follower_id, leader_id, dec!(0.5), dec!(10000));
assert!(result.is_ok());
assert_eq!(manager.get_follower_count(leader_id), 1);
}
#[test]
fn test_unfollow_trader() {
let mut manager = SocialTradingManager::new();
let leader_id = Uuid::new_v4();
let leader_profile = TraderProfile {
trader_id: leader_id,
username: "Leader".to_string(),
follower_count: 0,
win_rate: dec!(0.75),
total_pnl: dec!(10000),
avg_monthly_return: dec!(0.15),
max_drawdown: dec!(-500),
total_trades: 100,
commission_rate: dec!(0.10),
accepting_followers: true,
max_follower_exposure: dec!(50000),
};
manager.add_profile(leader_profile);
let follower_id = Uuid::new_v4();
let relationship_id = manager
.follow(follower_id, leader_id, dec!(0.5), dec!(10000))
.unwrap();
let result = manager.unfollow(relationship_id);
assert!(result.is_ok());
}
#[test]
fn test_get_copy_trades() {
let mut manager = SocialTradingManager::new();
let leader_id = Uuid::new_v4();
let leader_profile = TraderProfile {
trader_id: leader_id,
username: "Leader".to_string(),
follower_count: 0,
win_rate: dec!(0.75),
total_pnl: dec!(10000),
avg_monthly_return: dec!(0.15),
max_drawdown: dec!(-500),
total_trades: 100,
commission_rate: dec!(0.10),
accepting_followers: true,
max_follower_exposure: dec!(50000),
};
manager.add_profile(leader_profile);
let follower_id = Uuid::new_v4();
manager
.follow(follower_id, leader_id, dec!(0.5), dec!(10000))
.unwrap();
let mut balances = HashMap::new();
balances.insert(follower_id, dec!(5000));
let copy_trades = manager.get_copy_trades(leader_id, dec!(1000), &balances);
assert_eq!(copy_trades.len(), 1);
assert_eq!(copy_trades[0].0, follower_id);
}
#[test]
fn test_calculate_commission() {
let mut manager = SocialTradingManager::new();
let leader_id = Uuid::new_v4();
let leader_profile = TraderProfile {
trader_id: leader_id,
username: "Leader".to_string(),
follower_count: 0,
win_rate: dec!(0.75),
total_pnl: dec!(10000),
avg_monthly_return: dec!(0.15),
max_drawdown: dec!(-500),
total_trades: 100,
commission_rate: dec!(0.10),
accepting_followers: true,
max_follower_exposure: dec!(50000),
};
manager.add_profile(leader_profile);
let commission = manager.calculate_commission(leader_id, dec!(1000));
assert_eq!(commission, dec!(100));
let commission_loss = manager.calculate_commission(leader_id, dec!(-500));
assert_eq!(commission_loss, Decimal::ZERO);
}
#[test]
fn test_get_top_traders() {
let mut manager = SocialTradingManager::new();
for i in 1..=5 {
let leader_id = Uuid::new_v4();
let profile = TraderProfile {
trader_id: leader_id,
username: format!("Trader{}", i),
follower_count: 0,
win_rate: dec!(0.70),
total_pnl: dec!(5000),
avg_monthly_return: Decimal::from(i) / dec!(10), max_drawdown: dec!(-100),
total_trades: 50,
commission_rate: dec!(0.10),
accepting_followers: true,
max_follower_exposure: dec!(10000),
};
manager.add_profile(profile);
}
let top_traders = manager.get_top_traders(3);
assert_eq!(top_traders.len(), 3);
assert!(top_traders[0].avg_monthly_return >= top_traders[1].avg_monthly_return);
assert!(top_traders[1].avg_monthly_return >= top_traders[2].avg_monthly_return);
}
}