use serde::{Serialize, Deserialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum LiveMode {
Flat,
Single,
Both,
Recovery,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PositionSnapshot {
pub symbol: String,
pub long_qty: f64,
pub short_qty: f64,
pub entry_long: f64,
pub entry_short: f64,
pub initial_entry_price: f64,
pub hedge_entry_price: f64,
pub first_side: String,
pub entry_leverage: f64,
pub contract_value: f64,
pub long_margin: f64,
pub short_margin: f64,
pub used_margin: f64,
pub last_price: f64,
pub realized_pnl: f64,
pub cut_streak: u32,
pub base_qty: f64,
pub min_contract_qty: f64,
pub last_entry_time: i64, // Время последнего входа (Unix timestamp)
pub hedge_cooldown_sec: i64, // Cooldown период после входа (секунды)
pub last_cooldown_log_time: i64, // Время последнего лога cooldown (чтобы не спамить)
pub last_profit_rebuy_price: f64, // Цена последней перезакупки прибыльной стороны (для разворота -1%)
pub transitioning_to_single_from_both: bool, // Флаг: переходим из BOTH в Single (после полной резки убыточной стороны)
pub mode: LiveMode,
}
impl PositionSnapshot {
pub fn new(symbol: &str) -> Self {
Self {
symbol: symbol.to_string(),
long_qty: 0.0,
short_qty: 0.0,
entry_long: 0.0,
entry_short: 0.0,
initial_entry_price: 0.0,
hedge_entry_price: 0.0,
first_side: "".to_string(),
entry_leverage: 0.0,
contract_value: 1.0,
long_margin: 0.0,
short_margin: 0.0,
used_margin: 0.0,
last_price: 0.0,
realized_pnl: 0.0,
cut_streak: 0,
base_qty: 0.0,
min_contract_qty: 1.0,
last_entry_time: 0,
hedge_cooldown_sec: 5, // 5 секунд cooldown по умолчанию
last_cooldown_log_time: 0, // Инициализация
last_profit_rebuy_price: 0.0, // Инициализация
transitioning_to_single_from_both: false, // Инициализация
mode: LiveMode::Flat,
}
}
pub fn update_price(&mut self, px: f64) {
self.last_price = px;
}
pub fn unrealized(&self) -> f64 {
self.unrealized_at_price(self.last_price)
}
pub fn unrealized_at_price(&self, price: f64) -> f64 {
let mut p = 0.0;
if self.long_qty > 0.0 && self.entry_long > 0.0 {
p += (price - self.entry_long) * self.long_qty * self.contract_value;
}
if self.short_qty > 0.0 && self.entry_short > 0.0 {
p += (self.entry_short - price) * self.short_qty * self.contract_value;
}
p
}
pub fn total_pnl(&self) -> f64 {
self.realized_pnl + self.unrealized()
}
pub fn total_pnl_at_price(&self, price: f64) -> f64 {
self.realized_pnl + self.unrealized_at_price(price)
}
pub fn pnl_long(&self) -> f64 {
if self.long_qty > 0.0 && self.entry_long > 0.0 {
(self.last_price - self.entry_long) * self.long_qty * self.contract_value
} else {
0.0
}
}
pub fn pnl_short(&self) -> f64 {
if self.short_qty > 0.0 && self.entry_short > 0.0 {
(self.entry_short - self.last_price) * self.short_qty * self.contract_value
} else {
0.0
}
}
pub fn unrealized_long(&self) -> f64 {
if self.long_qty > 0.0 && self.entry_long > 0.0 {
(self.last_price - self.entry_long) * self.long_qty * self.contract_value
} else {
0.0
}
}
pub fn unrealized_short(&self) -> f64 {
if self.short_qty > 0.0 && self.entry_short > 0.0 {
(self.entry_short - self.last_price) * self.short_qty * self.contract_value
} else {
0.0
}
}
pub fn delta(&self) -> f64 {
self.long_qty - self.short_qty
}
pub fn is_hedged(&self) -> bool {
(self.long_qty - self.short_qty).abs() < f64::EPSILON
}
pub fn recalc_margins(&mut self) {
if self.entry_leverage > 0.0 {
if self.long_qty > 0.0 && self.entry_long > 0.0 {
let nominal = self.long_qty * self.entry_long * self.contract_value;
self.long_margin = nominal / self.entry_leverage;
} else {
self.long_margin = 0.0;
}
if self.short_qty > 0.0 && self.entry_short > 0.0 {
let nominal = self.short_qty * self.entry_short * self.contract_value;
self.short_margin = nominal / self.entry_leverage;
} else {
self.short_margin = 0.0;
}
} else {
self.long_margin = 0.0;
self.short_margin = 0.0;
}
self.used_margin = self.long_margin + self.short_margin;
}
pub fn open_long(&mut self, qty: f64, price: f64) {
if qty <= 0.0 {
return;
}
if self.long_qty == 0.0 {
self.entry_long = price;
self.long_qty = qty;
} else {
let prev_qty = self.long_qty;
let prev_entry = self.entry_long;
let new_qty = prev_qty + qty;
let new_entry = (prev_entry * prev_qty + price * qty) / new_qty;
self.long_qty = new_qty;
self.entry_long = new_entry;
}
if self.base_qty <= 0.0 {
self.base_qty = self.long_qty;
}
if self.first_side.is_empty() {
self.first_side = "LONG".into();
self.initial_entry_price = price;
}
self.mode = if self.short_qty > 0.0 {
LiveMode::Both
} else {
LiveMode::Single
};
// 🔥 Обновляем время входа для cooldown хеджа
self.last_entry_time = chrono::Utc::now().timestamp();
self.recalc_margins();
}
pub fn open_short(&mut self, qty: f64, price: f64) {
if qty <= 0.0 {
return;
}
if self.short_qty == 0.0 {
self.entry_short = price;
self.short_qty = qty;
} else {
let prev_qty = self.short_qty;
let prev_entry = self.entry_short;
let new_qty = prev_qty + qty;
let new_entry = (prev_entry * prev_qty + price * qty) / new_qty;
self.short_qty = new_qty;
self.entry_short = new_entry;
}
if self.base_qty <= 0.0 {
self.base_qty = self.short_qty;
}
if self.first_side.is_empty() {
self.first_side = "SHORT".into();
self.initial_entry_price = price;
}
self.mode = if self.long_qty > 0.0 {
LiveMode::Both
} else {
LiveMode::Single
};
// 🔥 Обновляем время входа для cooldown хеджа
self.last_entry_time = chrono::Utc::now().timestamp();
self.recalc_margins();
}
pub fn close_long(&mut self, qty: f64) {
if qty <= 0.0 || self.long_qty <= 0.0 {
return;
}
if qty >= self.long_qty {
self.long_qty = 0.0;
self.entry_long = 0.0;
self.long_margin = 0.0;
} else {
let prev_qty = self.long_qty;
let ratio = (prev_qty - qty) / prev_qty;
self.long_qty -= qty;
self.long_margin *= ratio.max(0.0);
}
self.recalc_margins();
self.mode = if self.long_qty > 0.0 && self.short_qty > 0.0 {
LiveMode::Both
} else if self.long_qty > 0.0 || self.short_qty > 0.0 {
LiveMode::Single
} else {
LiveMode::Flat
};
}
pub fn close_short(&mut self, qty: f64) {
if qty <= 0.0 || self.short_qty <= 0.0 {
return;
}
if qty >= self.short_qty {
self.short_qty = 0.0;
self.entry_short = 0.0;
self.short_margin = 0.0;
} else {
let prev_qty = self.short_qty;
let ratio = (prev_qty - qty) / prev_qty;
self.short_qty -= qty;
self.short_margin *= ratio.max(0.0);
}
self.recalc_margins();
self.mode = if self.long_qty > 0.0 && self.short_qty > 0.0 {
LiveMode::Both
} else if self.long_qty > 0.0 || self.short_qty > 0.0 {
LiveMode::Single
} else {
LiveMode::Flat
};
}
pub fn close_all_at_price(&mut self, price: f64) {
let cv = self.contract_value.max(1.0);
if self.long_qty > 0.0 && self.entry_long > 0.0 {
self.realized_pnl +=
(price - self.entry_long) * self.long_qty * cv;
}
if self.short_qty > 0.0 && self.entry_short > 0.0 {
self.realized_pnl +=
(self.entry_short - price) * self.short_qty * cv;
}
self.long_qty = 0.0;
self.short_qty = 0.0;
self.entry_long = 0.0;
self.entry_short = 0.0;
self.long_margin = 0.0;
self.short_margin = 0.0;
self.used_margin = 0.0;
self.mode = LiveMode::Flat;
}
}