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