← Назад к документации
logger
Исходный код Rust - Trading AI
📄 Rust
📦 Модуль
🔧 Исходный код
// ===========================
// File: src/live/logger.rs
// ===========================
//
// write_live_log() — стабильный, безопасный логер,
// который НЕ допускает "двойного PnL" и ghost-состояний.
// ===========================

use anyhow::Result;
use serde_json::json;
use std::sync::Arc;
use redis::AsyncCommands;

use crate::redis::{RedisCore, get_redis_conn};
use crate::live::state::PositionSnapshot;
use crate::live::meta::TradingMeta;
use crate::live::position_machine::PositionMachine;

/// ======================================================
/// write_live_log() — запись атомарного состояния в Redis
/// ======================================================
pub async fn write_live_log(
    redis: Arc<RedisCore>,
    pos: &PositionSnapshot,
    meta: &TradingMeta,
    machine: &PositionMachine,
) -> Result<()> {
    let mut conn = get_redis_conn(&redis).await?;

    // -------------------------------
    // 1. БЕЗОПАСНЫЕ ENTRY ЦЕНЫ
    // -------------------------------
    let entry_long = if pos.long_qty > 0.0 { pos.entry_long } else { 0.0 };
    let entry_short = if pos.short_qty > 0.0 { pos.entry_short } else { 0.0 };

    // Если qty > 0, а entry == 0 → не пишем лог (нестабильное состояние)
    if pos.long_qty > 0.0 && entry_long <= 0.0 {
        return Ok(());
    }
    if pos.short_qty > 0.0 && entry_short <= 0.0 {
        return Ok(());
    }

    // -------------------------------
    // 2. НОМИНАЛЫ
    // -------------------------------
    let cv = pos.contract_value.max(1.0);

    let long_notional = if pos.long_qty > 0.0 {
        pos.long_qty * entry_long * cv
    } else {
        0.0
    };

    let short_notional = if pos.short_qty > 0.0 {
        pos.short_qty * entry_short * cv
    } else {
        0.0
    };

    let total_notional = long_notional + short_notional;

    // -------------------------------
    // 3. PNL (ТОЛЬКО КОРРЕКТНЫЕ)
    // -------------------------------
    let pnl_long = if pos.long_qty > 0.0 {
        (pos.last_price - entry_long) * pos.long_qty * cv
    } else {
        0.0
    };

    let pnl_short = if pos.short_qty > 0.0 {
        (entry_short - pos.last_price) * pos.short_qty * cv
    } else {
        0.0
    };

    // Реализованный + нереализованный
    let unrealized = pnl_long + pnl_short;
    let total_pnl = pos.realized_pnl + unrealized;

    // -------------------------------
    // 4. ЗАЩИТА ОТ ФАЛЬШИВОГО PNL
    // -------------------------------

    // Невозможные ситуации не пишем
    if pnl_long > 0.0 && pnl_short > 0.0 && pos.long_qty > 0.0 && pos.short_qty > 0.0 {
        // BOTH не может одновременно давать +PNL на обе стороны
        return Ok(());
    }

    // -------------------------------
    // 5. ПРОЦЕНТНЫЕ PNL
    // -------------------------------
    let pnl_long_pct = if long_notional > 0.0 {
        pnl_long / long_notional * 100.0
    } else {
        0.0
    };

    let pnl_short_pct = if short_notional > 0.0 {
        pnl_short / short_notional * 100.0
    } else {
        0.0
    };

    let total_pnl_pct = if total_notional > 0.0 {
        total_pnl / total_notional * 100.0
    } else {
        0.0
    };

    // -------------------------------
    // 6. ФОРМИРОВАНИЕ ЛОГА
    // -------------------------------
    let log_value = json!({
        "ts": meta.ts,
        "symbol": pos.symbol,

        "price": meta.price,
        "last_price": pos.last_price,

        "long_qty": pos.long_qty,
        "short_qty": pos.short_qty,

        "entry_long": entry_long,
        "entry_short": entry_short,

        "long_notional": long_notional,
        "short_notional": short_notional,
        "total_notional": total_notional,

        "pnl_long": pnl_long,
        "pnl_short": pnl_short,
        "unrealized": unrealized,
        "realized_pnl": pos.realized_pnl,
        "total_pnl": total_pnl,

        "pnl_long_pct": pnl_long_pct,
        "pnl_short_pct": pnl_short_pct,
        "total_pnl_pct": total_pnl_pct,

        "delta": pos.delta(),
        "mode": format!("{:?}", pos.mode),

        "phase": format!("{:?}", machine.phase),
        "cut_count": machine.cut_count,
        "boosted_once": machine.boosted_once,

        "initial_entry_price": pos.initial_entry_price,
        "hedge_entry_price": pos.hedge_entry_price,
        "base_qty": pos.base_qty,
    });

    // -------------------------------
    // 7. ЗАПИСЬ В REDIS
    // -------------------------------
    let key = format!("hb:live_log:{}", pos.symbol);

    let _: () = conn.rpush(&key, serde_json::to_string(&log_value)?).await?;
    let _: () = conn.ltrim(&key, -2000, -1).await?;

    Ok(())
}