mod commands;
mod format;
use clap::{CommandFactory, Parser, Subcommand};
use clap_complete::{generate, Shell};
use format::OutputFormat;
use std::time::Instant;
#[derive(Parser)]
#[command(
name = "percli",
about = "Offline simulator for the Percolator risk engine",
long_about = "Simulate haircuts, liquidations, margin checks, and conservation proofs\n\
for the Percolator risk engine \u{2014} no chain required.\n\n\
Define scenarios in TOML, run simulations, and inspect how the risk\n\
engine handles stressed markets, undercollateralized accounts, and\n\
loss socialization.",
after_help = "See 'percli help <command>' for more on a specific command.",
version
)]
struct Cli {
#[command(subcommand)]
command: Commands,
#[arg(long, short, global = true)]
format: Option<OutputFormat>,
#[arg(long, global = true, default_value = "auto")]
color: String,
}
#[derive(Subcommand)]
enum Commands {
#[command(after_help = "Examples:\n \
percli init\n \
percli init --template liquidation\n \
percli init --template haircut")]
Init {
#[arg(long, default_value = "basic")]
template: String,
#[arg(long, short)]
output: Option<String>,
},
#[command(after_help = "Examples:\n \
percli sim scenario.toml\n \
percli sim scenario.toml --verbose\n \
percli sim scenario.toml --step-by-step\n \
percli sim scenario.toml --override maintenance_margin_bps=300")]
Sim {
path: String,
#[arg(long)]
step_by_step: bool,
#[arg(long)]
no_check_conservation: bool,
#[arg(long, short)]
verbose: bool,
#[arg(long = "override", value_name = "KEY=VALUE")]
overrides: Vec<String>,
},
#[command(after_help = "Examples:\n \
percli step --state engine.json deposit alice 100000\n \
percli step --state engine.json crank --oracle 1100 --slot 200\n \
percli step --state engine.json liquidate alice")]
Step {
#[arg(long)]
state: String,
#[command(subcommand)]
action: StepAction,
},
#[command(
after_help = "Metrics: summary, haircut, conservation, vault, equity, margin, position, accounts\n\n\
Examples:\n \
percli query --state engine.json summary\n \
percli query --state engine.json haircut\n \
percli query --state engine.json equity --account alice"
)]
Query {
#[arg(long)]
state: String,
metric: String,
#[arg(long)]
account: Option<String>,
},
#[command(after_help = "Examples:\n percli inspect scenario.toml")]
Inspect {
path: String,
},
#[command(after_help = "Examples:\n \
percli agent run --config agent.toml\n \
percli agent run --config agent.toml --verbose-ticks\n \
percli agent init --output agent.toml")]
Agent {
#[command(subcommand)]
action: AgentCommand,
},
#[cfg(feature = "chain")]
#[command(after_help = "Examples:\n \
percli chain deploy\n \
percli chain deposit --idx 0 --amount 1000000\n \
percli chain query market\n \
percli chain query 0\n \
percli chain crank --oracle 1100")]
Chain {
#[arg(long, global = true)]
rpc: Option<String>,
#[arg(long, global = true)]
keypair: Option<String>,
#[arg(long, global = true)]
program: Option<String>,
#[command(subcommand)]
action: commands::chain::ChainCommand,
},
#[cfg(feature = "chain")]
#[command(after_help = "Examples:\n \
percli keeper --interval 10\n \
percli keeper --rpc devnet --pyth-feed H6ARHf6YXhGYeQfUzQNGk6rDNnLBQKrenN712K4AQJEG")]
Keeper {
#[arg(long, global = true)]
rpc: Option<String>,
#[arg(long, global = true)]
keypair: Option<String>,
#[arg(long, global = true)]
program: Option<String>,
#[arg(long, default_value = "10")]
interval: u64,
#[arg(long)]
pyth_feed: String,
#[arg(long)]
json_logs: bool,
},
Completions {
shell: Shell,
},
}
#[derive(Subcommand)]
enum AgentCommand {
#[command(
after_help = "The agent process receives NDJSON on stdin and writes responses on stdout.\n\n\
Protocol:\n \
percli sends {\"type\":\"init\",...} once, then {\"type\":\"tick\",...} per tick.\n \
Agent responds with {\"actions\":[...]} or {\"type\":\"shutdown\"}.\n\n\
See https://github.com/kamiyoai/percli for protocol docs."
)]
Run {
#[arg(long)]
config: String,
#[arg(long)]
verbose_ticks: bool,
#[arg(long)]
dry_run: bool,
},
Init {
#[arg(long, short)]
output: Option<String>,
},
}
#[derive(Subcommand)]
enum StepAction {
Deposit { account: String, amount: u128 },
Withdraw { account: String, amount: u128 },
Trade {
long: String,
short: String,
#[arg(long)]
size: i128,
#[arg(long)]
price: u64,
},
Crank {
#[arg(long)]
oracle: u64,
#[arg(long)]
slot: u64,
},
Liquidate { account: String },
Settle { account: String },
SetOracle { price: u64 },
SetSlot { slot: u64 },
SetFundingRate { rate: i64 },
}
fn main() {
let cli = Cli::parse();
match cli.color.as_str() {
"never" => owo_colors::set_override(false),
"always" => owo_colors::set_override(true),
_ => {}
}
if let Err(e) = run(cli) {
format::status::error(&format!("{:#}", e));
std::process::exit(1);
}
}
fn run(cli: Cli) -> anyhow::Result<()> {
let fmt = cli.format.unwrap_or_else(OutputFormat::from_env);
let start = Instant::now();
match cli.command {
Commands::Init { template, output } => commands::init::run(&template, output.as_deref()),
Commands::Sim {
path,
step_by_step,
no_check_conservation,
verbose,
overrides,
} => {
let result = commands::sim::run(
&path,
fmt,
step_by_step,
!no_check_conservation,
verbose,
&overrides,
);
format::status::finished(start);
result
}
Commands::Step { state, action } => commands::step::run(&state, action, fmt),
Commands::Query {
state,
metric,
account,
} => commands::query::run(&state, &metric, account.as_deref(), fmt),
Commands::Inspect { path } => commands::inspect::run(&path),
Commands::Agent { action } => match action {
AgentCommand::Run {
config,
verbose_ticks,
dry_run,
} => commands::agent::run(std::path::Path::new(&config), verbose_ticks, dry_run),
AgentCommand::Init { output } => {
commands::agent::init(output.as_deref().map(std::path::Path::new))
}
},
#[cfg(feature = "chain")]
Commands::Chain {
rpc,
keypair,
program,
action,
} => commands::chain::run(
action,
rpc.as_deref(),
keypair.as_deref(),
program.as_deref(),
),
#[cfg(feature = "chain")]
Commands::Keeper {
rpc,
keypair,
program,
interval,
pyth_feed,
json_logs,
} => {
#[cfg(feature = "chain")]
{
if json_logs {
tracing_subscriber::fmt().json().init();
} else {
tracing_subscriber::fmt().init();
}
}
let config = percli_chain::ChainConfig::new(
rpc.as_deref(),
keypair.as_deref(),
program.as_deref(),
)?;
commands::keeper::run(&config, interval, &pyth_feed)
}
Commands::Completions { shell } => {
let mut cmd = Cli::command();
generate(shell, &mut cmd, "percli", &mut std::io::stdout());
Ok(())
}
}
}