From 831dc84ff9b7903612f4127afe3507fb7afef986 Mon Sep 17 00:00:00 2001 From: zastian-dev Date: Tue, 10 Feb 2026 15:29:50 +0000 Subject: [PATCH] daksjdkal --- CLAUDE.md | 62 ++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.lock | 50 +++++++++++++++++++------------------- src/alpaca.rs | 1 + src/dashboard.rs | 24 ++++++++++++++++--- 4 files changed, 109 insertions(+), 28 deletions(-) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..1b96a0c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,62 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Vibe Invest is a Rust algorithmic trading bot using the Alpaca paper trading API. It executes a hybrid momentum + mean-reversion strategy across 50 tech/financial stocks. It has two execution modes: live paper trading and historical backtesting, plus a web dashboard. + +## Build & Run Commands + +```bash +# Build +cargo build --release + +# Run live paper trading (requires .env with ALPACA_API_KEY and ALPACA_SECRET_KEY) +cargo run --release +cargo run --release -- --timeframe hourly + +# Run backtesting +cargo run --release -- --backtest --years 3 +cargo run --release -- --backtest --years 5 --capital 50000 +cargo run --release -- --backtest --years 1 --months 6 --timeframe hourly + +# Lint and format (available via nix flake) +cargo clippy +cargo fmt +``` + +There are no tests currently in the project. + +## Architecture + +``` +main.rs CLI parsing (clap), logging setup, mode routing +├── bot.rs Live trading loop: market hours detection, per-symbol analysis, +│ order execution, position tracking (JSON persistence) +├── backtester.rs Historical simulation: processes bars sequentially, tracks +│ positions, calculates metrics (CAGR, Sharpe, Sortino, drawdown) +├── alpaca.rs Alpaca REST API client with rate limiting (200 req/min via governor) +├── indicators.rs Technical indicators: EMA, SMA, RSI, MACD, ADX, ATR, +│ Bollinger Bands, ROC. Signal scoring algorithm in generate_signal() +├── dashboard.rs Axum web server (default port 5000) with Chart.js frontend +├── config.rs Strategy parameters, stock universe (50 symbols), risk limits +├── types.rs Domain types: Signal, TradeSignal, Trade, BacktestResult, Bar, etc. +└── paths.rs XDG-compliant file paths (~/.local/share/invest-bot/) +``` + +**Key data flow:** Both bot.rs and backtester.rs call `indicators::generate_signal()` which scores multiple indicators into a composite buy/sell signal. The bot executes via alpaca.rs; the backtester simulates internally. + +## Strategy Parameters (config.rs) + +- Hourly mode scales all indicator periods by 7x +- Risk: max 22% position size, 2.5% stop-loss, 40% take-profit, trailing stop at 7% after 12% gain +- Signal thresholds: StrongBuy ≥ 6.0, Buy ≥ 3.5, Sell ≤ -3.5, StrongSell ≤ -6.0 +- Backtester restricts to top 4 momentum stocks; live mode does not + +## Environment + +- Nix flake provides dev tools: rustc, cargo, clippy, rustfmt, rust-analyzer, cargo-watch +- `.env` file required: `ALPACA_API_KEY`, `ALPACA_SECRET_KEY`, optional `DASHBOARD_PORT` +- Persistent state stored in `~/.local/share/invest-bot/` (positions, equity history, logs) +- Backtest outputs: `backtest_equity_curve.csv`, `backtest_trades.csv` in working directory diff --git a/Cargo.lock b/Cargo.lock index 925ad50..24a587a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -819,31 +819,6 @@ dependencies = [ "hashbrown 0.16.1", ] -[[package]] -name = "invest-bot" -version = "0.1.0" -dependencies = [ - "anyhow", - "axum", - "chrono", - "clap", - "csv", - "dirs", - "dotenvy", - "governor", - "lazy_static", - "num-format", - "reqwest", - "serde", - "serde_json", - "thiserror 2.0.18", - "tokio", - "tower-http", - "tracing", - "tracing-appender", - "tracing-subscriber", -] - [[package]] name = "ipnet" version = "2.11.0" @@ -1965,6 +1940,31 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "vibe-invest" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum", + "chrono", + "clap", + "csv", + "dirs", + "dotenvy", + "governor", + "lazy_static", + "num-format", + "reqwest", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tower-http", + "tracing", + "tracing-appender", + "tracing-subscriber", +] + [[package]] name = "want" version = "0.3.1" diff --git a/src/alpaca.rs b/src/alpaca.rs index d3572f3..aeed3ac 100644 --- a/src/alpaca.rs +++ b/src/alpaca.rs @@ -68,6 +68,7 @@ pub struct Position { pub current_price: String, pub unrealized_pl: String, pub unrealized_plpc: String, + pub unrealized_intraday_pl: Option, pub change_today: Option, } diff --git a/src/dashboard.rs b/src/dashboard.rs index c888236..c3cdc77 100644 --- a/src/dashboard.rs +++ b/src/dashboard.rs @@ -27,6 +27,7 @@ struct AccountResponse { cash: f64, buying_power: f64, total_pnl: f64, + daily_pnl: f64, position_count: usize, } @@ -156,9 +157,13 @@ const HTML_TEMPLATE: &str = r#"
Buying Power
$0.00
+
+
Total P&L
+
$0.00
+
Today's P&L
-
$0.00
+
$0.00
Open Positions
@@ -206,9 +211,11 @@ const HTML_TEMPLATE: &str = r#" updateText('stat-portfolio-value', data.portfolio_value, true); updateText('stat-cash', data.cash, true); updateText('stat-buying-power', data.buying_power, true); - updateText('stat-pnl', data.total_pnl, true, true); + updateText('stat-total-pnl', data.total_pnl, true, true); + updateClass('stat-total-pnl', data.total_pnl); + updateText('stat-daily-pnl', data.daily_pnl, true, true); + updateClass('stat-daily-pnl', data.daily_pnl); updateText('stat-positions', data.position_count); - updateClass('stat-pnl', data.total_pnl); } catch (error) { console.error('Error loading account stats:', error); } } @@ -324,6 +331,7 @@ async fn api_account(State(state): State>) -> impl IntoRespo cash: 0.0, buying_power: 0.0, total_pnl: 0.0, + daily_pnl: 0.0, position_count: 0, }), ) @@ -341,11 +349,21 @@ async fn get_account_data(client: &AlpacaClient) -> anyhow::Result().ok()) .sum(); + let daily_pnl: f64 = positions + .iter() + .filter_map(|p| { + p.unrealized_intraday_pl + .as_ref() + .and_then(|s| s.parse::().ok()) + }) + .sum(); + Ok(AccountResponse { portfolio_value: account.portfolio_value.parse().unwrap_or(0.0), cash: account.cash.parse().unwrap_or(0.0), buying_power: account.buying_power.parse().unwrap_or(0.0), total_pnl, + daily_pnl, position_count: positions.len(), }) }