schwab-api-cli 0.1.5

schwab-api-cli — agent-first CLI for Charles Schwab Trader API (experimental — use at your own risk)
Documentation
use std::fs::OpenOptions;
use std::io::Write;
use std::path::Path;

use anyhow::{Context, Result};
use chrono::{DateTime, Utc};
use serde_json::Value;

use super::paths::{backtest_journal_path, journal_path, sim_journal_path};

pub fn append_event(rules_path: &Path, simulate: bool, event_type: &str, payload: Value) -> Result<()> {
    append_event_at(rules_path, simulate, Utc::now(), event_type, payload)
}

pub fn append_backtest_event_at(
    rules_path: &Path,
    at: DateTime<Utc>,
    event_type: &str,
    payload: Value,
) -> Result<()> {
    write_journal_line(&backtest_journal_path(rules_path), at, event_type, payload)
}

pub fn append_event_at(
    rules_path: &Path,
    simulate: bool,
    at: DateTime<Utc>,
    event_type: &str,
    payload: Value,
) -> Result<()> {
    let path = if simulate {
        sim_journal_path(rules_path)
    } else {
        journal_path(rules_path)
    };
    write_journal_line(&path, at, event_type, payload)
}

fn write_journal_line(
    path: &Path,
    at: DateTime<Utc>,
    event_type: &str,
    payload: Value,
) -> Result<()> {
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent)?;
    }
    let line = serde_json::json!({
        "ts": at.to_rfc3339(),
        "type": event_type,
        "payload": payload,
    });
    let mut file = OpenOptions::new()
        .create(true)
        .append(true)
        .open(path)
        .with_context(|| format!("open journal {}", path.display()))?;
    writeln!(file, "{line}")?;
    Ok(())
}

pub fn read_all_backtest(rules_path: &Path) -> Result<Vec<Value>> {
    let path = backtest_journal_path(rules_path);
    if !path.exists() {
        return Ok(vec![]);
    }
    let raw = std::fs::read_to_string(&path)
        .with_context(|| format!("read backtest journal {}", path.display()))?;
    let mut out = Vec::new();
    for line in raw.lines() {
        let line = line.trim();
        if line.is_empty() {
            continue;
        }
        out.push(serde_json::from_str(line)?);
    }
    Ok(out)
}

pub fn clear_backtest_journal(rules_path: &Path) -> Result<()> {
    let path = backtest_journal_path(rules_path);
    if path.exists() {
        std::fs::remove_file(&path)
            .with_context(|| format!("clear backtest journal {}", path.display()))?;
    }
    Ok(())
}