← Назад к документации
state
Исходный код Rust - Trading AI
📄 Rust
📦 Модуль
🔧 Исходный код
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;
    }
}