← Назад к документации
strategy_both
Исходный код Rust - Trading AI
📄 Rust
📦 Модуль
🔧 Исходный код
// ===============================
// File: src/live/strategy_both.rs
// ===============================
//
// BOTH-стратегия по модели VolatilityCycle:
//
//  - работает ТОЛЬКО когда открыты обе стороны (LONG и SHORT)
//  - определяет момент резки через VolatilityCycle::check()
//  - исполняет 100% / 50% резки и пересборку объёмов
//  - управляется фазовой машиной PositionMachine
//  - при развороте тренда и уходе против БОЛЬШЕЙ стороны на -1.5% → зеркальный хедж
//
// Фазы:
//   Single      — односторонняя позиция (не трогаем здесь)
//   Both        — базовый BOTH
//   Boosted     — после первого BOOST-цикла
//   WindDown    — последующие резки (сужение позиции)
//   ExitReady   — финальный выход при небольшом плюсе
// ===============================

use crate::live::state::PositionSnapshot;
use crate::live::meta::TradingMeta;
use crate::live::position_machine::{PositionMachine, PositionPhase};
use crate::live::volatility_cycle::VolatilityCycle;
use crate::live::actions::LiveAction;

/// Порог для авто-ре-хеджа по PnL% БОЛЬШЕЙ стороны.
const REHEDGE_PNL_TRIGGER_PCT: f64 = -1.5;

/// Порог выхода в фазе EXIT_READY (прибыль в % от базового номинала).
const EXIT_READY_PROFIT_PCT: f64 = 0.5;

pub struct BothStrategy;

impl BothStrategy {
    pub fn new() -> Self {
        Self
    }

    /// Авто-ре-хедж при развороте тренда:
    ///
    ///  - смотрим, какая сторона сейчас БОЛЬШАЯ по объёму (long или short)
    ///  - считаем её PnL% относительно собственной цены входа
    ///  - если PnL% этой стороны ≤ -1.5% → немедленно выравниваем объёмы (зеркальный хедж)
    ///
    /// Возвращает Vec<LiveAction> с ордерами на выравнивание.
    fn auto_rehedge_on_reverse(
        &self,
        pos: &mut PositionSnapshot,
        meta: &TradingMeta,
        machine: &mut PositionMachine,
    ) -> Vec<LiveAction> {
        let mut actions = Vec::new();

        // должны быть обе стороны
        if !(pos.long_qty > 0.0 && pos.short_qty > 0.0) {
            return actions;
        }

        // если объёмы примерно равны — дополнительный хедж не нужен
        if (pos.long_qty - pos.short_qty).abs() < f64::EPSILON {
            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 (bigger_is_long, bigger_qty, bigger_entry) = if pos.long_qty > pos.short_qty {
            (true, pos.long_qty, pos.entry_long)
        } else {
            (false, pos.short_qty, pos.entry_short)
        };

        if bigger_qty <= 0.0 || bigger_entry <= 0.0 {
            return actions;
        }

        let notional = bigger_qty * bigger_entry * cv;
        if notional <= 0.0 {
            return actions;
        }

        // PnL% бОльшей стороны
        let pnl_abs = if bigger_is_long {
            (price - bigger_entry) * bigger_qty * cv
        } else {
            (bigger_entry - price) * bigger_qty * cv
        };

        let pnl_pct = (pnl_abs / notional) * 100.0;

        // Если ещё не достигли отрицательных -1.5% — ничего не делаем
        if pnl_pct > REHEDGE_PNL_TRIGGER_PCT {
            return actions;
        }

        // Зеркальный хедж: выравниваем объёмы
        if bigger_is_long {
            // LONG больше, чем SHORT → нарастить SHORT до LONG
            let need_short = (pos.long_qty - pos.short_qty).max(0.0);
            if need_short > 0.0 {
                actions.push(LiveAction::OpenShort(need_short, price));
            }
        } else {
            // SHORT больше, чем LONG → нарастить LONG до SHORT
            let need_long = (pos.short_qty - pos.long_qty).max(0.0);
            if need_long > 0.0 {
                actions.push(LiveAction::OpenLong(need_long, price));
            }
        }

        // обновляем базовый юнит и состояние машины
        pos.base_qty = pos.long_qty.min(pos.short_qty);
        machine.phase = PositionPhase::Both;
        machine.cut_count = 0;
        machine.boosted_once = false;

        actions
    }

    /// Основной обработчик BOTH / BOOSTED / WINDDOWN / EXIT_READY.
    /// Возвращает Vec<LiveAction> для исполнения через create_real_orders.
    pub fn process(
        &self,
        pos: &mut PositionSnapshot,
        meta: &TradingMeta,
        machine: &mut PositionMachine,
    ) -> Vec<LiveAction> {
        let mut actions = Vec::new();

        // актуализируем фазу на каждом тике
        machine.update_phase(pos);

        match machine.phase {
            // односторонний режим обрабатывается SingleStrategy
            PositionPhase::Single => {
                return actions;
            }

            // основной BOTH-поток: обычные резки + авто-ре-хедж
            PositionPhase::Both | PositionPhase::Boosted | PositionPhase::WindDown => {
                let price = if pos.last_price > 0.0 {
                    pos.last_price
                } else {
                    meta.price
                };
                if price <= 0.0 {
                    return actions;
                }

                // ========================================
                // РАСЧЁТ И ЛОГИРОВАНИЕ PnL ПО СТОРОНАМ
                // ========================================

                let now = chrono::Utc::now().timestamp();

                // 1. PnL LONG в %
                let pnl_long_abs = pos.unrealized_long();
                let pnl_long_pct = if pos.long_qty > 0.0 && pos.entry_long > 0.0 {
                    let notional_long = pos.long_qty * pos.entry_long * pos.contract_value.max(1.0);
                    (pnl_long_abs / notional_long) * 100.0
                } else {
                    0.0
                };

                // 2. PnL SHORT в %
                let pnl_short_abs = pos.unrealized_short();
                let pnl_short_pct = if pos.short_qty > 0.0 && pos.entry_short > 0.0 {
                    let notional_short = pos.short_qty * pos.entry_short * pos.contract_value.max(1.0);
                    (pnl_short_abs / notional_short) * 100.0
                } else {
                    0.0
                };

                // 3. Дельта (разница)
                let delta_pct = pnl_long_pct + pnl_short_pct;

                // 4. Порог резки
                let trigger_pct = if machine.boosted_once { 1.5 } else { 3.0 };

                // 5. Комиссии и фандинг
                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;

                // 6. Чистые PnL (за вычетом комиссий)
                let pnl_long_clean = pnl_long_pct - cost_pct;
                let pnl_short_clean = pnl_short_pct - cost_pct;

                // Логируем каждые 5 секунд
                if now - pos.last_cooldown_log_time >= 5 {
                    println!("   📊 BOTH СТАТУС:");
                    println!("      LONG:  {:+.2}% (сырой: {:+.2}%, чистый: {:+.2}%)", pnl_long_pct, pnl_long_pct, pnl_long_clean);
                    println!("      SHORT: {:+.2}% (сырой: {:+.2}%, чистый: {:+.2}%)", pnl_short_pct, pnl_short_pct, pnl_short_clean);
                    println!("      Дельта: {:+.2}%", delta_pct);
                    println!("      Порог резки: {:.1}% (boosted_once={})", trigger_pct, machine.boosted_once);
                    println!("      Комиссии+фандинг: {:.3}%", cost_pct);

                    // Индикатор готовности к резке
                    let profit_side_clean = if pnl_long_clean > pnl_short_clean {
                        pnl_long_clean
                    } else {
                        pnl_short_clean
                    };

                    if profit_side_clean > 0.0 && profit_side_clean >= trigger_pct * 0.8 {
                        let progress_pct = (profit_side_clean / trigger_pct) * 100.0;
                        println!("      ⚠️ БЛИЗО К РЕЗКЕ: {:.1}% от порога!", progress_pct);
                    }

                    pos.last_cooldown_log_time = now;
                }

                // 1) Проверяем возможность резки по VolatilityCycle (3% / 1.5% чистыми)
                if let Some(sig) = VolatilityCycle::check(
                    pos,
                    meta,
                    pos.entry_long,
                    pos.entry_short,
                    machine.boosted_once,
                ) {
                    let cut_actions = VolatilityCycle::execute(pos, &sig, price, meta);
                    actions.extend(cut_actions);
                    machine.on_cut();
                    machine.update_phase(pos);
                }

                // 2) Проверяем разворот на -1% от цены последней перезакупки прибыльной стороны
                if pos.last_profit_rebuy_price > 0.0 {
                    let reverse_threshold = pos.last_profit_rebuy_price * 0.99;  // -1%

                    if price <= reverse_threshold {
                        // Цена развернулась на -1% → выравниваем позиции (зеркальный хедж)
                        println!("🔄 РАЗВОРОТ НА -1%: цена {:.6} <= {:.6} (last_profit_rebuy_price * 0.99)",
                            price, reverse_threshold);

                        // Определяем какая сторона больше
                        if pos.long_qty > pos.short_qty && pos.short_qty > 0.0 {
                            // LONG больше → докупаем SHORT до LONG
                            let need_short = (pos.long_qty - pos.short_qty).max(0.0);
                            if need_short > 0.0 {
                                actions.push(LiveAction::OpenShort(need_short, price));
                                println!("   ✅ Выравнивание: добавлено {:.0} SHORT, теперь {:.0}/{:.0}",
                                    need_short, pos.long_qty, pos.short_qty);
                            }
                        } else if pos.short_qty > pos.long_qty && pos.long_qty > 0.0 {
                            // SHORT больше → докупаем LONG до SHORT
                            let need_long = (pos.short_qty - pos.long_qty).max(0.0);
                            if need_long > 0.0 {
                                actions.push(LiveAction::OpenLong(need_long, price));
                                println!("   ✅ Выравнивание: добавлено {:.0} LONG, теперь {:.0}/{:.0}",
                                    need_long, pos.long_qty, pos.short_qty);
                            }
                        }

                        // Обновляем базовый юнит
                        pos.base_qty = pos.long_qty.min(pos.short_qty);
                    }
                }

                // 3) Проверяем авто-ре-хедж на развороте против бОльшей стороны
                let rehedge_actions = self.auto_rehedge_on_reverse(pos, meta, machine);
                actions.extend(rehedge_actions);
            }

            // финальная фаза — выходим при небольшом плюсе по всей конструкции
            PositionPhase::ExitReady => {
                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 base = pos.long_qty.min(pos.short_qty);

                // базовый номинал по минимальной стороне
                let base_notional = base * price * cv;
                if base_notional <= 0.0 {
                    return actions;
                }

                let net = pos.total_pnl_at_price(price);
                let profit_pct = (net / base_notional) * 100.0;

                // маленький, но уверенный плюс → закрываем всё
                if profit_pct >= EXIT_READY_PROFIT_PCT {
                    // 🔥🔥 Создаём actions для закрытия вместо прямого вызова close_all_at_price
                    if pos.long_qty > 0.0 {
                        actions.push(LiveAction::CloseLong(pos.long_qty, price, false));
                    }
                    if pos.short_qty > 0.0 {
                        actions.push(LiveAction::CloseShort(pos.short_qty, price, false));
                    }
                    machine.on_exit();
                }
            }
        }

        actions
    }
}