// ===============================
// 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
}
}