percli 1.0.0

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

const STATE_VERSION: u32 = 2;

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "op", rename_all = "snake_case")]
pub enum Operation {
    Deposit {
        account: String,
        amount: u64,
    },
    Withdraw {
        account: String,
        amount: u64,
    },
    Trade {
        long: String,
        short: String,
        size_q: i64,
        price: u64,
    },
    Crank {
        oracle: u64,
        slot: u64,
    },
    SetOracle {
        price: u64,
    },
    SetSlot {
        slot: u64,
    },
    SetFundingRate {
        rate: i64,
    },
    Liquidate {
        account: String,
    },
    Settle {
        account: String,
    },
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SavedState {
    pub version: u32,
    pub params: ParamsConfig,
    pub oracle_price: u64,
    pub slot: u64,
    pub funding_rate: i64,
    pub operations: Vec<Operation>,
}

impl SavedState {
    pub fn new(params: ParamsConfig, oracle_price: u64, slot: u64) -> Self {
        Self {
            version: STATE_VERSION,
            params,
            oracle_price,
            slot,
            funding_rate: 0,
            operations: Vec::new(),
        }
    }
}

pub fn load_or_create(path: &Path) -> Result<SavedState> {
    if path.exists() {
        let content = std::fs::read_to_string(path)
            .with_context(|| format!("failed to read state file: {}", path.display()))?;
        let state: SavedState = serde_json::from_str(&content)
            .with_context(|| format!("failed to parse state file: {}", path.display()))?;
        if state.version < STATE_VERSION {
            anyhow::bail!(
                "state file version {} is too old (expected {}). \
                 delete the file and recreate it",
                state.version,
                STATE_VERSION,
            );
        }
        Ok(state)
    } else {
        Ok(SavedState::new(ParamsConfig::default(), 1000, 0))
    }
}

pub fn save(path: &Path, state: &SavedState) -> Result<()> {
    let json = serde_json::to_string_pretty(state)?;
    let tmp = path.with_extension("json.tmp");
    std::fs::write(&tmp, &json)
        .with_context(|| format!("failed to write temp state file: {}", tmp.display()))?;
    std::fs::rename(&tmp, path)
        .with_context(|| format!("failed to rename state file: {}", path.display()))?;
    Ok(())
}

pub fn replay(state: &SavedState) -> Result<NamedEngine> {
    let params = state.params.to_risk_params();
    let mut engine = NamedEngine::new(params, state.slot, state.oracle_price);
    engine.set_funding_rate(state.funding_rate);

    for (i, op) in state.operations.iter().enumerate() {
        replay_op(&mut engine, op)
            .with_context(|| format!("failed replaying operation {}", i + 1))?;
    }

    Ok(engine)
}

fn replay_op(engine: &mut NamedEngine, op: &Operation) -> Result<()> {
    match op {
        Operation::Deposit { account, amount } => {
            engine.deposit(account, *amount as u128)?;
        }
        Operation::Withdraw { account, amount } => {
            engine.withdraw(account, *amount as u128)?;
        }
        Operation::Trade {
            long,
            short,
            size_q,
            price,
        } => {
            engine.trade(long, short, *size_q as i128, *price)?;
        }
        Operation::Crank { oracle, slot } => {
            engine.crank(*oracle, *slot)?;
        }
        Operation::SetOracle { price } => {
            engine.set_oracle(*price)?;
        }
        Operation::SetSlot { slot } => {
            engine.set_slot(*slot);
        }
        Operation::SetFundingRate { rate } => {
            engine.set_funding_rate(*rate);
        }
        Operation::Liquidate { account } => {
            engine.liquidate(account)?;
        }
        Operation::Settle { account } => {
            engine.settle(account)?;
        }
    }
    Ok(())
}