// ===========================
// File: src/live/actions.rs
// ===========================
//
// LiveAction + применение к PositionSnapshot
//
use crate::live::state::{PositionSnapshot, LiveMode};
/// Действия LIVE-движка.
///
/// Важное разделение:
/// - CloseLong/CloseShort НИЧЕГО НЕ ПИШУТ в realized_pnl
/// (PNL добавляется отдельным действием Realize(pnl)).
/// - SetBaseQty — новое действие: запоминаем базовый юнит позиции
///
#[derive(Clone, Debug)]
pub enum LiveAction {
/// Базовое количество контрактов для всей сессии
SetBaseQty(f64),
/// Задать плечо на сессию (20x, 50x, ...)
SetLeverage(f64),
/// Задать contract_value (quanto_multiplier)
SetContractValue(f64),
/// Открыть/докупить лонг (qty контрактов по цене price)
OpenLong(f64, f64),
/// Открыть/докупить шорт
OpenShort(f64, f64),
/// Закрыть часть/весь лонг (qty, price, is_profit_side)
CloseLong(f64, f64, bool),
/// Закрыть часть/весь шорт (qty, price, is_profit_side)
CloseShort(f64, f64, bool),
/// Добавить в реализованный PnL
Realize(f64),
/// Сменить режим (Flat/Single/Both/Recovery)
SetMode(LiveMode),
/// Сбросить счётчик резок
ResetCutStreak,
}
/// Применение списка действий к позиции.
pub fn apply_to_position(pos: &mut PositionSnapshot, actions: &[LiveAction]) {
for act in actions {
match act {
// === Новое действие: Запоминаем базовый юнит ===
LiveAction::SetBaseQty(q) => {
pos.base_qty = (*q).max(0.0);
}
LiveAction::SetLeverage(lev) => {
pos.entry_leverage = *lev;
pos.recalc_margins();
}
LiveAction::SetContractValue(v) => {
pos.contract_value = if *v > 0.0 { *v } else { 1.0 };
pos.recalc_margins();
}
LiveAction::OpenLong(qty, price) => {
if *qty <= 0.0 {
continue;
}
// Используем PositionSnapshot::open_long, чтобы
// корректно поддерживать first_side, initial_entry_price,
// base_qty и mode.
pos.open_long(*qty, *price);
}
LiveAction::OpenShort(qty, price) => {
if *qty <= 0.0 {
continue;
}
pos.open_short(*qty, *price);
}
LiveAction::CloseLong(qty, _price, _is_profit) => {
if *qty <= 0.0 || pos.long_qty <= 0.0 {
continue;
}
// Сколько реально можем закрыть (не больше текущего объёма)
let close_qty = qty.min(pos.long_qty);
if close_qty >= pos.long_qty {
// Закрываем весь лонг
pos.long_qty = 0.0;
pos.entry_long = 0.0;
pos.long_margin = 0.0;
} else {
let prev_qty = pos.long_qty;
let new_qty = prev_qty - close_qty;
let ratio = if prev_qty > 0.0 {
new_qty / prev_qty
} else {
0.0
};
pos.long_qty = new_qty.max(0.0);
pos.long_margin *= ratio.max(0.0);
}
pos.recalc_margins();
pos.mode = if pos.long_qty > 0.0 && pos.short_qty > 0.0 {
LiveMode::Both
} else if pos.long_qty > 0.0 || pos.short_qty > 0.0 {
LiveMode::Single
} else {
LiveMode::Flat
};
}
LiveAction::CloseShort(qty, _price, _is_profit) => {
if *qty <= 0.0 || pos.short_qty <= 0.0 {
continue;
}
let close_qty = qty.min(pos.short_qty);
if close_qty >= pos.short_qty {
// Закрываем весь шорт
pos.short_qty = 0.0;
pos.entry_short = 0.0;
pos.short_margin = 0.0;
} else {
let prev_qty = pos.short_qty;
let new_qty = prev_qty - close_qty;
let ratio = if prev_qty > 0.0 {
new_qty / prev_qty
} else {
0.0
};
pos.short_qty = new_qty.max(0.0);
pos.short_margin *= ratio.max(0.0);
}
pos.recalc_margins();
pos.mode = if pos.long_qty > 0.0 && pos.short_qty > 0.0 {
LiveMode::Both
} else if pos.long_qty > 0.0 || pos.short_qty > 0.0 {
LiveMode::Single
} else {
LiveMode::Flat
};
}
LiveAction::Realize(pnl) => {
pos.realized_pnl += pnl;
}
LiveAction::SetMode(m) => {
pos.mode = m.clone();
}
LiveAction::ResetCutStreak => {
pos.cut_streak = 0;
}
}
}
}