1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use strum::{Display, EnumString};
4use uuid::Uuid;
5
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
7pub struct Bot {
8 pub id: Uuid,
9 pub account_id: Uuid,
10 pub name: String,
11 pub persona: Persona,
12 pub status: BotStatus,
13 pub droplet_id: Option<i64>,
14 pub desired_config_version_id: Option<Uuid>,
15 pub applied_config_version_id: Option<Uuid>,
16 pub registration_token: Option<String>,
17 pub created_at: DateTime<Utc>,
18 pub updated_at: DateTime<Utc>,
19 pub last_heartbeat_at: Option<DateTime<Utc>>,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Display, EnumString)]
23#[strum(serialize_all = "snake_case")]
24pub enum Persona {
25 Beginner,
26 Tweaker,
27 QuantLite,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Display, EnumString)]
31#[strum(serialize_all = "snake_case")]
32pub enum BotStatus {
33 Pending,
34 Provisioning,
35 Online,
36 Paused,
37 Error,
38 Destroyed,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct BotConfig {
43 pub id: Uuid,
44 pub bot_id: Uuid,
45 pub version: i32,
46 pub trading_config: TradingConfig,
47 pub risk_config: RiskConfig,
48 pub secrets: BotSecrets,
49 pub created_at: DateTime<Utc>,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct StoredBotConfig {
54 pub id: Uuid,
55 pub bot_id: Uuid,
56 pub version: i32,
57 pub trading_config: TradingConfig,
58 pub risk_config: RiskConfig,
59 pub secrets: EncryptedBotSecrets,
60 pub created_at: DateTime<Utc>,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct TradingConfig {
65 pub asset_focus: AssetFocus,
66 pub algorithm: AlgorithmMode,
67 pub strictness: StrictnessLevel,
68 pub paper_mode: bool,
69 pub signal_knobs: Option<SignalKnobs>,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub enum AssetFocus {
74 Majors,
75 Memes,
76 Custom(Vec<String>),
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub enum AlgorithmMode {
81 Trend,
82 MeanReversion,
83 Breakout,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub enum StrictnessLevel {
88 Low,
89 Medium,
90 High,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct SignalKnobs {
95 pub volume_confirmation: bool,
96 pub volatility_brake: bool,
97 pub liquidity_filter: StrictnessLevel,
98 pub correlation_brake: bool,
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct RiskConfig {
103 pub max_position_size_pct: f64,
104 pub max_daily_loss_pct: f64,
105 pub max_drawdown_pct: f64,
106 pub max_trades_per_day: i32,
107}
108
109impl RiskConfig {
110 pub fn validate(&self) -> Result<(), Vec<String>> {
111 let mut errors = Vec::new();
112
113 if !(0.0..=100.0).contains(&self.max_position_size_pct) {
114 errors.push(format!(
115 "max_position_size_pct must be between 0 and 100, got {}",
116 self.max_position_size_pct
117 ));
118 }
119
120 if !(0.0..=100.0).contains(&self.max_daily_loss_pct) {
121 errors.push(format!(
122 "max_daily_loss_pct must be between 0 and 100, got {}",
123 self.max_daily_loss_pct
124 ));
125 }
126
127 if !(0.0..=100.0).contains(&self.max_drawdown_pct) {
128 errors.push(format!(
129 "max_drawdown_pct must be between 0 and 100, got {}",
130 self.max_drawdown_pct
131 ));
132 }
133
134 if self.max_trades_per_day < 0 {
135 errors.push(format!(
136 "max_trades_per_day must be >= 0, got {}",
137 self.max_trades_per_day
138 ));
139 }
140
141 if errors.is_empty() {
142 Ok(())
143 } else {
144 Err(errors)
145 }
146 }
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct BotSecrets {
151 pub llm_provider: String,
152 pub llm_api_key: String,
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct EncryptedBotSecrets {
157 pub llm_provider: String,
158 #[serde(with = "serde_bytes")]
159 pub llm_api_key_encrypted: Vec<u8>,
160}
161
162impl Bot {
163 pub fn new(account_id: Uuid, name: String, persona: Persona) -> Self {
164 let now = Utc::now();
165 Self {
166 id: Uuid::new_v4(),
167 account_id,
168 name,
169 persona,
170 status: BotStatus::Pending,
171 droplet_id: None,
172 desired_config_version_id: None,
173 applied_config_version_id: None,
174 registration_token: None,
175 created_at: now,
176 updated_at: now,
177 last_heartbeat_at: None,
178 }
179 }
180}