daksjdkal
This commit is contained in:
62
CLAUDE.md
Normal file
62
CLAUDE.md
Normal file
@@ -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
|
||||||
50
Cargo.lock
generated
50
Cargo.lock
generated
@@ -819,31 +819,6 @@ dependencies = [
|
|||||||
"hashbrown 0.16.1",
|
"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]]
|
[[package]]
|
||||||
name = "ipnet"
|
name = "ipnet"
|
||||||
version = "2.11.0"
|
version = "2.11.0"
|
||||||
@@ -1965,6 +1940,31 @@ version = "0.2.15"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
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]]
|
[[package]]
|
||||||
name = "want"
|
name = "want"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ pub struct Position {
|
|||||||
pub current_price: String,
|
pub current_price: String,
|
||||||
pub unrealized_pl: String,
|
pub unrealized_pl: String,
|
||||||
pub unrealized_plpc: String,
|
pub unrealized_plpc: String,
|
||||||
|
pub unrealized_intraday_pl: Option<String>,
|
||||||
pub change_today: Option<String>,
|
pub change_today: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ struct AccountResponse {
|
|||||||
cash: f64,
|
cash: f64,
|
||||||
buying_power: f64,
|
buying_power: f64,
|
||||||
total_pnl: f64,
|
total_pnl: f64,
|
||||||
|
daily_pnl: f64,
|
||||||
position_count: usize,
|
position_count: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,9 +157,13 @@ const HTML_TEMPLATE: &str = r#"<!DOCTYPE html>
|
|||||||
<div class="stat-label">Buying Power</div>
|
<div class="stat-label">Buying Power</div>
|
||||||
<div class="stat-value" id="stat-buying-power">$0.00</div>
|
<div class="stat-value" id="stat-buying-power">$0.00</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">Total P&L</div>
|
||||||
|
<div class="stat-value" id="stat-total-pnl">$0.00</div>
|
||||||
|
</div>
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="stat-label">Today's P&L</div>
|
<div class="stat-label">Today's P&L</div>
|
||||||
<div class="stat-value" id="stat-pnl">$0.00</div>
|
<div class="stat-value" id="stat-daily-pnl">$0.00</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="stat-label">Open Positions</div>
|
<div class="stat-label">Open Positions</div>
|
||||||
@@ -206,9 +211,11 @@ const HTML_TEMPLATE: &str = r#"<!DOCTYPE html>
|
|||||||
updateText('stat-portfolio-value', data.portfolio_value, true);
|
updateText('stat-portfolio-value', data.portfolio_value, true);
|
||||||
updateText('stat-cash', data.cash, true);
|
updateText('stat-cash', data.cash, true);
|
||||||
updateText('stat-buying-power', data.buying_power, 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);
|
updateText('stat-positions', data.position_count);
|
||||||
updateClass('stat-pnl', data.total_pnl);
|
|
||||||
|
|
||||||
} catch (error) { console.error('Error loading account stats:', error); }
|
} catch (error) { console.error('Error loading account stats:', error); }
|
||||||
}
|
}
|
||||||
@@ -324,6 +331,7 @@ async fn api_account(State(state): State<Arc<DashboardState>>) -> impl IntoRespo
|
|||||||
cash: 0.0,
|
cash: 0.0,
|
||||||
buying_power: 0.0,
|
buying_power: 0.0,
|
||||||
total_pnl: 0.0,
|
total_pnl: 0.0,
|
||||||
|
daily_pnl: 0.0,
|
||||||
position_count: 0,
|
position_count: 0,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -341,11 +349,21 @@ async fn get_account_data(client: &AlpacaClient) -> anyhow::Result<AccountRespon
|
|||||||
.filter_map(|p| p.unrealized_pl.parse::<f64>().ok())
|
.filter_map(|p| p.unrealized_pl.parse::<f64>().ok())
|
||||||
.sum();
|
.sum();
|
||||||
|
|
||||||
|
let daily_pnl: f64 = positions
|
||||||
|
.iter()
|
||||||
|
.filter_map(|p| {
|
||||||
|
p.unrealized_intraday_pl
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|s| s.parse::<f64>().ok())
|
||||||
|
})
|
||||||
|
.sum();
|
||||||
|
|
||||||
Ok(AccountResponse {
|
Ok(AccountResponse {
|
||||||
portfolio_value: account.portfolio_value.parse().unwrap_or(0.0),
|
portfolio_value: account.portfolio_value.parse().unwrap_or(0.0),
|
||||||
cash: account.cash.parse().unwrap_or(0.0),
|
cash: account.cash.parse().unwrap_or(0.0),
|
||||||
buying_power: account.buying_power.parse().unwrap_or(0.0),
|
buying_power: account.buying_power.parse().unwrap_or(0.0),
|
||||||
total_pnl,
|
total_pnl,
|
||||||
|
daily_pnl,
|
||||||
position_count: positions.len(),
|
position_count: positions.len(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user