schwab-api-cli 0.1.0

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

use anyhow::Result;
use chrono::{DateTime, NaiveDate, Utc};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};

use crate::options::types::StrategyKind;

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct AgentState {
    pub agent_id: String,
    pub last_tick: Option<DateTime<Utc>>,
    pub trades_today: u32,
    pub trades_day: Option<NaiveDate>,
    pub open_positions: HashMap<String, TrackedPosition>,
    pub last_actions: Vec<AgentAction>,
    pub pending_order_ids: Vec<String>,
    #[serde(default)]
    pub tick_count: u64,
    #[serde(default)]
    pub last_llm_review_tick: Option<u64>,
    #[serde(default)]
    pub llm_review_count: u64,
    #[serde(default)]
    pub last_llm_summary: Option<Value>,
    #[serde(default)]
    pub last_session: Option<String>,
    #[serde(default)]
    pub regular_tick_count: u64,
    #[serde(default)]
    pub last_overnight_digest_at: Option<DateTime<Utc>>,
    #[serde(default)]
    pub open_playbook: Option<Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrackedPosition {
    pub position_id: String,
    pub account_hash: String,
    pub underlying: String,
    pub expiry: String,
    pub strategy: String,
    pub opened_at: DateTime<Utc>,
    pub entry_credit: Option<f64>,
    pub max_loss_usd: f64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentAction {
    pub at: DateTime<Utc>,
    pub action: String,
    pub detail: Value,
}

pub fn load_state(path: &Path) -> Result<AgentState> {
    if !path.exists() {
        return Ok(AgentState::default());
    }
    let content = fs::read_to_string(path)?;
    Ok(serde_json::from_str(&content)?)
}

pub fn save_state(path: &Path, state: &AgentState) -> Result<()> {
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)?;
    }
    let content = serde_json::to_string_pretty(state)?;
    fs::write(path, content)?;
    Ok(())
}

impl AgentState {
    pub fn reset_daily_if_needed(&mut self, today: NaiveDate) {
        if self.trades_day != Some(today) {
            self.trades_today = 0;
            self.trades_day = Some(today);
        }
    }

    pub fn record_action(&mut self, action: &str, detail: Value) {
        self.last_actions.push(AgentAction {
            at: Utc::now(),
            action: action.to_string(),
            detail,
        });
        if self.last_actions.len() > 100 {
            let drain = self.last_actions.len() - 100;
            self.last_actions.drain(0..drain);
        }
    }

    pub fn count_open_for_strategy(&self, account_hash: &str, strategy: StrategyKind) -> u32 {
        self.open_positions
            .values()
            .filter(|p| p.account_hash == account_hash && p.strategy == strategy.as_str())
            .count() as u32
    }
}

pub fn state_summary(state: &AgentState) -> Value {
    json!({
        "agent_id": state.agent_id,
        "last_tick": state.last_tick,
        "trades_today": state.trades_today,
        "open_positions": state.open_positions.len(),
        "tick_count": state.tick_count,
        "last_llm_review_tick": state.last_llm_review_tick,
        "last_llm_summary": state.last_llm_summary,
        "last_session": state.last_session,
        "regular_tick_count": state.regular_tick_count,
        "last_overnight_digest_at": state.last_overnight_digest_at,
        "open_playbook": state.open_playbook,
        "pending_orders": state.pending_order_ids.len(),
        "recent_actions": state.last_actions.iter().rev().take(10).collect::<Vec<_>>(),
    })
}