← Назад к документации
strategy_single
Исходный код Rust - Trading AI
📄 Rust
📦 Модуль
🔧 Исходный код
// ===============================
// File: src/live/strategy_single.rs
// ===============================
//
// Логика одностороннего входа и перехода в BOTH.
//
// Правила:
//   • +3% чистого PnL → резка 100% прибыльной стороны и создание структуры BOTH.
//   • После первой резки → следующие резки по +1.5%.
//   • При пересечении прибыли вниз через 0% → немедленный зеркальный хедж.
//   • Возвращает Vec<LiveAction> для исполнения через create_real_orders
//
// ===============================

use crate::live::meta::TradingMeta;
use crate::live::state::PositionSnapshot;
use crate::live::actions::LiveAction;

const FIRST_TRIGGER_PCT: f64 = 3.0;   // первая резка
const NEXT_TRIGGER_PCT: f64 = 1.5;    // последующие резки (закрыть всё)
const HEDGE_TRIGGER_PCT: f64 = -1.5;   // хедж при -1.5% от точки пересборки

pub struct SingleStrategy {
    pub did_cut_once: bool,
    pub first_rebuild_price: f64,  // Цена первой пересборки (для расчёта второй резки/хеджа)
}

impl SingleStrategy {
    pub fn new() -> Self {
        Self {
            did_cut_once: false,
            first_rebuild_price: 0.0,
        }
    }

    fn reset(&mut self) {
        self.did_cut_once = false;
        self.first_rebuild_price = 0.0;
    }

    pub fn process(
        &mut self,
        pos: &mut PositionSnapshot,
        meta: &TradingMeta,
    ) -> Vec<LiveAction> {
        let mut actions = Vec::new();

        // Только когда одна сторона активна
        if pos.long_qty > 0.0 && pos.short_qty > 0.0 {
            return actions; // BOTH — skip
        }

        if pos.long_qty <= 0.0 && pos.short_qty <= 0.0 {
            self.reset();
            return actions;
        }

        // 🔥🔥 ПРОВЕРКА: Пришли из BOTH после полной резки убыточной стороны?
        if pos.transitioning_to_single_from_both {
            let price = if pos.last_price > 0.0 { pos.last_price } else { meta.price };
            println!("🎯 SINGLE СТРАТЕГИЯ: Переход из BOTH режима (после полной резки убыточной стороны)");
            println!("   - Устанавливаем did_cut_once = true (пропускаем +3%)");
            println!("   - Устанавливаем first_rebuild_price = {:.6} (текущая цена)", price);
            println!("   - Ждём: +1.5% → CLOSE ALL, -1.5% → ХЕДЖ");

            self.did_cut_once = true;
            self.first_rebuild_price = price;
            pos.transitioning_to_single_from_both = false;  // Сбрасываем флаг
        }

        // 🔥🔥 COOLDOWN: Не резать прибыль N секунд после входа!
        let now = chrono::Utc::now().timestamp();
        let elapsed = now - pos.last_entry_time;

        if elapsed < pos.hedge_cooldown_sec {
            return actions;
        }

        let price = if pos.last_price > 0.0 { pos.last_price } else { meta.price };
        if price <= 0.0 {
            return actions;
        }

        let cv = pos.contract_value.max(1.0);

        let (is_long, qty, entry, unrl) = if pos.long_qty > 0.0 {
            (true, pos.long_qty, pos.entry_long, pos.unrealized_long())
        } else {
            (false, pos.short_qty, pos.entry_short, pos.unrealized_short())
        };

        if qty <= 0.0 || entry <= 0.0 {
            return actions;
        }

        let notional = qty * entry * cv;
        if notional <= 0.0 {
            return actions;
        }

        let pnl_pct_raw = (unrl / notional) * 100.0;

        // УЧЁТ КОМИССИЙ
        let fee_rate = meta
            .taker_fee_rate
            .abs()
            .max(meta.maker_fee_rate.abs())
            .max(0.0003);

        let funding = meta.funding_rate.abs();
        let cost_pct = (fee_rate * 2.0 + funding * 2.0) * 100.0;

        let pnl_pct = pnl_pct_raw - cost_pct;

        // ========================================
        // СЦЕНАРИЙ 1: ПЕРВАЯ РЕЗКА (+3%)
        // ========================================
        if !self.did_cut_once && pnl_pct >= FIRST_TRIGGER_PCT {
            println!(
                "✂️ ПЕРВАЯ РЕЗКА: {} side profit hit {:.2}% (raw={:.2}%, fees={:.3}%)",
                if is_long { "LONG" } else { "SHORT" },
                pnl_pct, pnl_pct_raw, cost_pct
            );

            let half_qty = qty / 2.0;

            // Закрываем ПОЛОВИНУ
            if is_long {
                actions.push(LiveAction::CloseLong(half_qty, price, true));
            } else {
                actions.push(LiveAction::CloseShort(half_qty, price, true));
            }

            // Сразу докупляем ПОЛОВИНУ
            if is_long {
                actions.push(LiveAction::OpenLong(half_qty, price));
            } else {
                actions.push(LiveAction::OpenShort(half_qty, price));
            }

            // Запоминаем цену пересборки для дальнейших проверок
            self.did_cut_once = true;
            self.first_rebuild_price = price;

            println!(
                "   🔄 ПЕРЕСБОРКА: зафиксировали прибыль, размер остался {} (как был)",
                qty
            );

            return actions;
        }

        // ========================================
        // СЦЕНАРИЙ 2: ПОСЛЕ ПЕРВОЙ РЕЗКИ
        // ========================================
        if self.did_cut_once && self.first_rebuild_price > 0.0 {
            // Считаем delta от точки пересборки
            let delta_from_rebuild = if is_long {
                (price - self.first_rebuild_price) / self.first_rebuild_price * 100.0
            } else {
                (self.first_rebuild_price - price) / self.first_rebuild_price * 100.0
            };

            // -----------------------------------
            // Вариант A: Ещё +1.5% → CLOSE ALL
            // -----------------------------------
            if delta_from_rebuild >= NEXT_TRIGGER_PCT {
                println!(
                    "✂️ ВТОРАЯ РЕЗКА: ещё {:.2}% от точки пересборки → CLOSE ALL",
                    delta_from_rebuild
                );

                // Закрываем ВСЁ
                if is_long {
                    actions.push(LiveAction::CloseLong(qty, price, true));
                } else {
                    actions.push(LiveAction::CloseShort(qty, price, true));
                }

                // Сбрасываем и ищем новую пару
                self.reset();
                return actions;
            }

            // -----------------------------------
            // Вариант B: -1.5% → Полный хедж
            // -----------------------------------
            if delta_from_rebuild <= HEDGE_TRIGGER_PCT {
                println!(
                    "🚨 ХЕДЖ: {:.2}% от точки пересборки → полный хедж",
                    delta_from_rebuild
                );

                // Открываем противоположную сторону того же размера
                if is_long {
                    actions.push(LiveAction::OpenShort(qty, price));
                } else {
                    actions.push(LiveAction::OpenLong(qty, price));
                }

                // Переходим в BOTH режим
                self.reset();
                return actions;
            }

            // Логируем текущее состояние раз в 5 секунд
            if now - pos.last_cooldown_log_time >= 5 {
                println!(
                    "   📊 От точки пересборки: {:.2}% (need +{:.1}% to close, {:.1}% to hedge)",
                    delta_from_rebuild,
                    NEXT_TRIGGER_PCT,
                    HEDGE_TRIGGER_PCT.abs()
                );
                pos.last_cooldown_log_time = now;
            }
        }

        actions
    }
}