percli 0.1.0

Offline CLI simulator for the Percolator risk engine
use anyhow::{Context, Result};
use percli_core::{NamedEngine, POS_SCALE};
use std::path::Path;

use crate::format::{self, OutputFormat};
use crate::StepAction;

pub fn run(state_path: &str, action: StepAction, fmt: OutputFormat) -> Result<()> {
    let path = Path::new(state_path);

    // Load or create engine state
    let mut state: SavedState = if path.exists() {
        let content = std::fs::read_to_string(path)
            .with_context(|| format!("failed to read state file: {state_path}"))?;
        serde_json::from_str::<SavedState>(&content)
            .with_context(|| format!("failed to parse state file: {state_path}"))?
    } else {
        // Create new state with defaults
        let params = percli_core::ParamsConfig::default();
        SavedState {
            params,
            oracle_price: 1000,
            slot: 0,
            funding_rate: 0,
            accounts: std::collections::BTreeMap::new(),
            deposits: Vec::new(),
            trades: Vec::new(),
        }
    };

    // Replay state into engine
    let risk_params = state.params.to_risk_params();
    let mut engine = NamedEngine::new(risk_params, state.slot, state.oracle_price);
    engine.set_funding_rate(state.funding_rate);

    // Replay deposits
    for dep in &state.deposits {
        engine.deposit(&dep.account, dep.amount)?;
    }

    // Replay trades
    for trade in &state.trades {
        engine.trade(
            &trade.long,
            &trade.short,
            trade.size_q,
            trade.price,
        )?;
    }

    // Execute the new action
    match &action {
        StepAction::Deposit { account, amount } => {
            engine.deposit(account, *amount)?;
            state.deposits.push(DepositRecord {
                account: account.clone(),
                amount: *amount,
            });
        }
        StepAction::Withdraw { account, amount } => {
            engine.withdraw(account, *amount)?;
            // Track as negative deposit for replay
            state.deposits.push(DepositRecord {
                account: account.clone(),
                amount: 0, // withdrawal tracked differently
            });
        }
        StepAction::Trade { long, short, size, price } => {
            let size_q = *size * POS_SCALE as i128;
            engine.trade(long, short, size_q, *price)?;
            state.trades.push(TradeRecord {
                long: long.clone(),
                short: short.clone(),
                size_q,
                price: *price,
            });
        }
        StepAction::Crank { oracle, slot } => {
            engine.crank(*oracle, *slot)?;
            state.oracle_price = *oracle;
            state.slot = *slot;
        }
        StepAction::Liquidate { account } => {
            let did = engine.liquidate(account)?;
            if !did {
                eprintln!("warning: {account} was not liquidatable");
            }
        }
        StepAction::SetOracle { price } => {
            engine.set_oracle(*price);
            state.oracle_price = *price;
        }
        StepAction::SetSlot { slot } => {
            engine.set_slot(*slot);
            state.slot = *slot;
        }
    }

    // Save state
    let state_json = serde_json::to_string_pretty(&state)?;
    std::fs::write(path, &state_json)
        .with_context(|| format!("failed to write state file: {state_path}"))?;

    // Print snapshot
    let snap = engine.snapshot();
    format::print_snapshot(&snap, fmt);

    Ok(())
}

use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
pub struct SavedState {
    pub params: percli_core::ParamsConfig,
    pub oracle_price: u64,
    pub slot: u64,
    pub funding_rate: i64,
    pub accounts: std::collections::BTreeMap<String, u16>,
    pub deposits: Vec<DepositRecord>,
    pub trades: Vec<TradeRecord>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct DepositRecord {
    pub account: String,
    pub amount: u128,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct TradeRecord {
    pub long: String,
    pub short: String,
    pub size_q: i128,
    pub price: u64,
}