← Назад к документации
step
Исходный код Rust - Trading AI
📄 Rust
📦 Модуль
🔧 Исходный код
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!("🎯🎯 ВСЕ ДЕЙСТВИЯ ВЫПОЛНЕНЫ");
}