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