use anyhow::Result;
use std::sync::Arc;
use crate::config::Config;
use crate::redis::RedisCore;
use crate::live::state::PositionSnapshot;
use crate::live::meta::TradingMeta;
use crate::live::position_machine::PositionMachine;
use crate::live::strategy_core::CoreStrategy;
use crate::live::strategy_single::SingleStrategy;
use crate::live::strategy_both::BothStrategy;
use crate::live::risk::risk_check;
use crate::gate::GateClient;
use crate::live::logger::write_live_log;
use crate::live::hedge_guard::HedgeGuard;
use crate::live::actions::LiveAction;
/// ======================================================
/// LiveStepContext — состояние между тиками LIVE
/// ======================================================
pub struct LiveStepContext {
pub machine: PositionMachine,
pub core: CoreStrategy,
pub single: SingleStrategy,
pub both: BothStrategy,
}
impl LiveStepContext {
pub fn new(cfg: &Config) -> Self {
Self {
machine: PositionMachine::new(),
core: CoreStrategy::new(cfg),
single: SingleStrategy::new(),
both: BothStrategy::new(),
}
}
}
/// ======================================================
/// LIVE STEP — главный шаг движка
/// ======================================================
pub async fn live_step(
redis: Arc<RedisCore>,
ctx: &mut LiveStepContext,
pos: &mut PositionSnapshot,
meta_prev: &TradingMeta,
meta_now: &TradingMeta,
current_balance: f64,
logger: &mut crate::logger::Logger,
) -> Result<()> {
// 🔥🔥 ПЕРЕДАЁМ REDIS В CORE STRATEGY ДЛЯ ЧТЕНИЯ/ЗАПИСИ РАСЧЁТА ВХОДА
// Это критично! Без этого entry_calculator не сможет сохранять/читать данные
pos.update_price(meta_now.price);
pos.update_price(meta_now.price);
let _risk = risk_check(pos, meta_prev, meta_now);
// FIX: CORE работает только если позиция полностью пустая
if pos.long_qty == 0.0 && pos.short_qty == 0.0 {
let gate = GateClient::new();
// 🔥🔥 ПЕРЕДАЁМ REDIS В CORE STRATEGY
let core_actions =
ctx.core.process_with_real_limits(redis.clone(), &gate, pos, meta_now, current_balance).await;
create_real_orders(redis.clone(), pos, &core_actions, &gate, logger).await;
}
// 🔥 SingleStrategy теперь возвращает actions!
let gate = GateClient::new();
let single_actions = ctx.single.process(pos, meta_now);
if !single_actions.is_empty() {
create_real_orders(redis.clone(), pos, &single_actions, &gate, logger).await;
}
let entry_for_hedge = if pos.initial_entry_price > 0.0 {
pos.initial_entry_price
} else if pos.long_qty > 0.0 {
pos.entry_long
} else if pos.short_qty > 0.0 {
pos.entry_short
} else {
0.0
};
// 🔥 HedgeGuard теперь возвращает actions!
if entry_for_hedge > 0.0 {
if let Some(sig) = HedgeGuard::check(pos, meta_now, entry_for_hedge) {
let hedge_actions = HedgeGuard::execute(pos, sig, meta_now.price);
if !hedge_actions.is_empty() {
create_real_orders(redis.clone(), pos, &hedge_actions, &gate, logger).await;
}
}
}
ctx.machine.update_phase(pos);
let both_actions = ctx.both.process(pos, meta_now, &mut ctx.machine);
if !both_actions.is_empty() {
create_real_orders(redis.clone(), pos, &both_actions, &gate, logger).await;
// 🔥 Логируем детальное состояние ПОСЛЕ выполнения действий
if pos.long_qty > 0.0 && pos.short_qty > 0.0 {
let cv = pos.contract_value.max(1.0);
let price = meta_now.price;
let pnl_long = if pos.entry_long > 0.0 {
pos.long_qty * (price - pos.entry_long) * cv
} else {
0.0
};
let pnl_short = if pos.entry_short > 0.0 {
pos.short_qty * (pos.entry_short - price) * cv
} else {
0.0
};
let notional_long = pos.long_qty * pos.entry_long * cv;
let notional_short = pos.short_qty * pos.entry_short * cv;
let pnl_long_pct = if notional_long > 0.0 {
(pnl_long / notional_long) * 100.0
} else {
0.0
};
let pnl_short_pct = if notional_short > 0.0 {
(pnl_short / notional_short) * 100.0
} else {
0.0
};
println!("📊 СТАТУС ПОСЛЕ ДЕЙСТВИЙ:");
println!(" LONG: {:+.2}% (qty={:.0}, entry={:.6}, realized={:.4})", pnl_long_pct, pos.long_qty, pos.entry_long, pos.realized_pnl);
println!(" SHORT: {:+.2}% (qty={:.0}, entry={:.6}, mode={:?})", pnl_short_pct, pos.short_qty, pos.entry_short, pos.mode);
} else {
println!("📊 СТАТУС ПОСЛЕ ДЕЙСТВИЙ: LONG={} SHORT={} mode={:?} realized_pnl={:.4}",
pos.long_qty, pos.short_qty, pos.mode, pos.realized_pnl);
}
}
write_live_log(redis, pos, meta_now, &ctx.machine).await?;
Ok(())
}
/// ======================================================
/// Создание РЕАЛЬНЫХ ордеров через Gate.io API (ВСЁ MARKET)
/// ======================================================
async fn create_real_orders(
_redis: Arc<RedisCore>,
pos: &mut PositionSnapshot,
actions: &[LiveAction],
gate: &GateClient,
logger: &mut crate::logger::Logger,
) {
use std::sync::Mutex;
use std::time::{Duration, Instant};
static LAST_ORDER_TIME: Mutex<Option<Instant>> = Mutex::new(None);
{
let mut last_time = LAST_ORDER_TIME.lock().unwrap();
if let Some(prev) = *last_time {
if prev.elapsed() < Duration::from_millis(200) {
return;
}
}
*last_time = Some(Instant::now());
}
let contract = pos.symbol.clone();
// ============================================
// 🎯 ФАЗА 1: Закрываем ПРИБЫЛЬНУЮ сторону (is_profit_side=true)
// ============================================
println!("🎯🎯 ФАЗА 1: Закрытие ПРИБЫЛЬНОЙ стороны...");
for action in actions {
match action {
LiveAction::CloseLong(qty, _, true) => {
if *qty <= 0.0 {
continue;
}
println!("🔴 CLOSE LONG (ПРИБЫЛЬ) {} {}", qty, contract);
match gate.create_reduce_only_order(&contract, false, *qty).await {
Ok(resp) => {
println!("✅ CLOSE LONG исполнен: {:?}", resp);
let fill_price = resp["fill_price"]
.as_str()
.and_then(|s| s.parse::<f64>().ok())
.unwrap_or(pos.last_price);
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 {
pos.long_qty -= close_qty;
pos.long_margin *= (pos.long_qty / (pos.long_qty + close_qty)).max(0.0);
}
pos.recalc_margins();
println!("📊 ПРИБЫЛЬНАЯ сторона закрыта: LONG {} по {}", close_qty, fill_price);
let _ = logger.log_action(
"CLOSE_LONG",
fill_price,
close_qty,
&format!("Закрыт LONG {} по {}", close_qty, fill_price),
);
}
Err(e) => {
println!("❌ Ошибка CLOSE LONG (ПРИБЫЛЬ): {:?}", e);
}
}
}
LiveAction::CloseShort(qty, _, true) => {
if *qty <= 0.0 {
continue;
}
println!("🔴 CLOSE SHORT (ПРИБЫЛЬ) {} {}", qty, contract);
match gate.create_reduce_only_order(&contract, true, *qty).await {
Ok(resp) => {
println!("✅ CLOSE SHORT исполнен: {:?}", resp);
let fill_price = resp["fill_price"]
.as_str()
.and_then(|s| s.parse::<f64>().ok())
.unwrap_or(pos.last_price);
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 {
pos.short_qty -= close_qty;
pos.short_margin *= (pos.short_qty / (pos.short_qty + close_qty)).max(0.0);
}
pos.recalc_margins();
println!("📊 ПРИБЫЛЬНАЯ сторона закрыта: SHORT {} по {}", close_qty, fill_price);
let _ = logger.log_action(
"CLOSE_SHORT",
fill_price,
close_qty,
&format!("Закрыт SHORT {} по {}", close_qty, fill_price),
);
}
Err(e) => {
println!("❌ Ошибка CLOSE SHORT (ПРИБЫЛЬ): {:?}", e);
}
}
}
_ => {}
}
}
// ============================================
// 🎯 ФАЗА 2: Закрываем УБЫТОЧНУЮ сторону (is_profit_side=false)
// ============================================
println!("🎯🎯 ФАЗА 2: Закрытие УБЫТОЧНОЙ стороны...");
for action in actions {
match action {
LiveAction::CloseLong(qty, _, false) => {
if *qty <= 0.0 {
continue;
}
println!("🔴 CLOSE LONG (УБЫТОК) {} {}", qty, contract);
match gate.create_reduce_only_order(&contract, false, *qty).await {
Ok(resp) => {
println!("✅ CLOSE LONG исполнен: {:?}", resp);
let fill_price = resp["fill_price"]
.as_str()
.and_then(|s| s.parse::<f64>().ok())
.unwrap_or(pos.last_price);
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 {
pos.long_qty -= close_qty;
pos.long_margin *= (pos.long_qty / (pos.long_qty + close_qty)).max(0.0);
}
pos.recalc_margins();
println!("📊 УБЫТОЧНАЯ сторона закрыта: LONG {} по {}", close_qty, fill_price);
let _ = logger.log_action(
"CLOSE_LONG",
fill_price,
close_qty,
&format!("Закрыт LONG {} по {}", close_qty, fill_price),
);
}
Err(e) => {
println!("❌ Ошибка CLOSE LONG (УБЫТОК): {:?}", e);
}
}
}
LiveAction::CloseShort(qty, _, false) => {
if *qty <= 0.0 {
continue;
}
println!("🔴 CLOSE SHORT (УБЫТОК) {} {}", qty, contract);
match gate.create_reduce_only_order(&contract, true, *qty).await {
Ok(resp) => {
println!("✅ CLOSE SHORT исполнен: {:?}", resp);
let fill_price = resp["fill_price"]
.as_str()
.and_then(|s| s.parse::<f64>().ok())
.unwrap_or(pos.last_price);
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 {
pos.short_qty -= close_qty;
pos.short_margin *= (pos.short_qty / (pos.short_qty + close_qty)).max(0.0);
}
pos.recalc_margins();
println!("📊 УБЫТОЧНАЯ сторона закрыта: SHORT {} по {}", close_qty, fill_price);
let _ = logger.log_action(
"CLOSE_SHORT",
fill_price,
close_qty,
&format!("Закрыт SHORT {} по {}", close_qty, fill_price),
);
}
Err(e) => {
println!("❌ Ошибка CLOSE SHORT (УБЫТОК): {:?}", e);
}
}
}
_ => {}
}
}
// ============================================
// 🎯 ФАЗА 3: Все остальные действия (OPEN, SET, etc.)
// ============================================
println!("🎯🎯 ФАЗА 3: Остальные действия...");
for action in actions {
match action {
LiveAction::OpenLong(qty, _) => {
if *qty <= 0.0 {
continue;
}
println!("🚀 MARKET LONG {} {}", qty, contract);
match gate.create_market_order(&contract, true, *qty).await {
Ok(resp) => {
println!("✅ MARKET LONG исполнен: {:?}", resp);
let fill_price = resp["fill_price"]
.as_str()
.and_then(|s| s.parse::<f64>().ok())
.unwrap_or(pos.last_price);
pos.open_long(*qty, fill_price);
println!("📊 Позиция обновлена: LONG {} по {}", *qty, fill_price);
let _ = logger.log_action(
"OPEN_LONG",
fill_price,
*qty,
&format!("Открыт LONG по {}", fill_price),
);
}
Err(e) => {
println!("❌ Ошибка MARKET LONG: {:?}", e);
}
}
}
LiveAction::OpenShort(qty, _) => {
if *qty <= 0.0 {
continue;
}
println!("🚀 MARKET SHORT {} {}", qty, contract);
match gate.create_market_order(&contract, false, *qty).await {
Ok(resp) => {
println!("✅ MARKET SHORT исполнен: {:?}", resp);
let fill_price = resp["fill_price"]
.as_str()
.and_then(|s| s.parse::<f64>().ok())
.unwrap_or(pos.last_price);
pos.open_short(*qty, fill_price);
println!("📊 Позиция обновлена: SHORT {} по {}", *qty, fill_price);
let _ = logger.log_action(
"OPEN_SHORT",
fill_price,
*qty,
&format!("Открыт SHORT по {}", fill_price),
);
}
Err(e) => {
println!("❌ Ошибка MARKET SHORT: {:?}", e);
}
}
}
LiveAction::SetBaseQty(qty) => {
pos.base_qty = (*qty).max(0.0);
println!("✅ Установлен базовый размер: {}", pos.base_qty);
}
LiveAction::SetLeverage(lev) => {
match gate.update_leverage(&contract, *lev).await {
Ok(_) => {
pos.entry_leverage = *lev;
pos.recalc_margins();
println!("✅ Плечо установлено {}x", pos.entry_leverage);
}
Err(e) => {
println!("⚠️ Плечо (уже установлено или ошибка): {}", e);
pos.entry_leverage = *lev;
pos.recalc_margins();
}
}
}
LiveAction::SetContractValue(val) => {
pos.contract_value = if *val > 0.0 { *val } else { 1.0 };
pos.recalc_margins();
println!("✅ Стоимость контракта: {}", pos.contract_value);
}
LiveAction::Realize(pnl) => {
pos.realized_pnl += pnl;
println!("✅ Реализован PnL: {}", pnl);
}
LiveAction::SetMode(mode) => {
pos.mode = mode.clone();
println!("✅ Режим изменён на {:?}", mode);
}
LiveAction::ResetCutStreak => {
pos.cut_streak = 0;
println!("✅ Сброс счётчика резок");
}
// Close actions уже обработаны в фазах 1 и 2
LiveAction::CloseLong(_, _, _) | LiveAction::CloseShort(_, _, _) => {
// Ничего не делаем - уже обработано выше
}
}
}
println!("🎯🎯 ВСЕ ДЕЙСТВИЯ ВЫПОЛНЕНЫ");
}