From 4476c04512230f2c04280290c875606d5ce4b387 Mon Sep 17 00:00:00 2001 From: mrfluffy Date: Wed, 25 Feb 2026 20:04:58 +0000 Subject: [PATCH] atr tracking --- .../j9250k63yp54q9r2m0xnca8lxjcfadv0-source | 1 + .../vanbyn1mbsqmff9in675grd5lqpr69zl-source | 1 - ...5d5b61aa8a61b7d9d765e1daf971a9a578f1cfa.rc | 6 +- src/bot.rs | 8 ++ src/dashboard.rs | 85 ++++++++++++++----- src/main.rs | 16 +++- 6 files changed, 91 insertions(+), 26 deletions(-) create mode 120000 .direnv/flake-inputs/j9250k63yp54q9r2m0xnca8lxjcfadv0-source delete mode 120000 .direnv/flake-inputs/vanbyn1mbsqmff9in675grd5lqpr69zl-source diff --git a/.direnv/flake-inputs/j9250k63yp54q9r2m0xnca8lxjcfadv0-source b/.direnv/flake-inputs/j9250k63yp54q9r2m0xnca8lxjcfadv0-source new file mode 120000 index 0000000..9deb22b --- /dev/null +++ b/.direnv/flake-inputs/j9250k63yp54q9r2m0xnca8lxjcfadv0-source @@ -0,0 +1 @@ +/nix/store/j9250k63yp54q9r2m0xnca8lxjcfadv0-source \ No newline at end of file diff --git a/.direnv/flake-inputs/vanbyn1mbsqmff9in675grd5lqpr69zl-source b/.direnv/flake-inputs/vanbyn1mbsqmff9in675grd5lqpr69zl-source deleted file mode 120000 index 8a560b2..0000000 --- a/.direnv/flake-inputs/vanbyn1mbsqmff9in675grd5lqpr69zl-source +++ /dev/null @@ -1 +0,0 @@ -/nix/store/vanbyn1mbsqmff9in675grd5lqpr69zl-source \ No newline at end of file diff --git a/.direnv/flake-profile-a5d5b61aa8a61b7d9d765e1daf971a9a578f1cfa.rc b/.direnv/flake-profile-a5d5b61aa8a61b7d9d765e1daf971a9a578f1cfa.rc index 8c7f69d..016ccb0 100644 --- a/.direnv/flake-profile-a5d5b61aa8a61b7d9d765e1daf971a9a578f1cfa.rc +++ b/.direnv/flake-profile-a5d5b61aa8a61b7d9d765e1daf971a9a578f1cfa.rc @@ -41,7 +41,7 @@ NIX_ENFORCE_NO_NATIVE='1' export NIX_ENFORCE_NO_NATIVE NIX_HARDENING_ENABLE='bindnow format fortify fortify3 libcxxhardeningextensive libcxxhardeningfast pic relro stackclashprotection stackprotector strictoverflow zerocallusedregs' export NIX_HARDENING_ENABLE -NIX_LDFLAGS='-rpath /home/work/Documents/rust/invest-bot/outputs/out/lib -L/nix/store/2w8pppicnsddp3n61ar96cnv3r6iyrdh-rust-mixed/lib -L/nix/store/g7lir5wb7g1a31szgw6n5wa0kbsq04zd-openssl-3.6.0/lib -L/nix/store/2w8pppicnsddp3n61ar96cnv3r6iyrdh-rust-mixed/lib -L/nix/store/g7lir5wb7g1a31szgw6n5wa0kbsq04zd-openssl-3.6.0/lib' +NIX_LDFLAGS='-rpath /home/mrfluffy/Documents/projects/rust/vibe-invest/outputs/out/lib -L/nix/store/2w8pppicnsddp3n61ar96cnv3r6iyrdh-rust-mixed/lib -L/nix/store/g7lir5wb7g1a31szgw6n5wa0kbsq04zd-openssl-3.6.0/lib -L/nix/store/2w8pppicnsddp3n61ar96cnv3r6iyrdh-rust-mixed/lib -L/nix/store/g7lir5wb7g1a31szgw6n5wa0kbsq04zd-openssl-3.6.0/lib' export NIX_LDFLAGS NIX_NO_SELF_RPATH='1' NIX_PKG_CONFIG_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu='1' @@ -142,7 +142,7 @@ name='nix-shell-env' export name nativeBuildInputs='/nix/store/2w8pppicnsddp3n61ar96cnv3r6iyrdh-rust-mixed /nix/store/fgm3pz8486ksh3f94629lpb7xjr2wjp7-openssl-3.6.0-dev /nix/store/rvp7qlpf5jqvdckjy1afjb6aha6j8dxg-pkg-config-wrapper-0.29.2 /nix/store/fl02yv3ax1qf1xkq64ik8qz5bjxyyd71-cargo-deny-0.19.0 /nix/store/7va1z8il76ycxvyvgsbpr4bjk89lzj5a-cargo-edit-0.13.8 /nix/store/zrx7kmcgzax4s6fldam9hf6nmwcw5nks-cargo-watch-8.5.3 /nix/store/b42adwrm8v2lb1889x1zb8dxzf5ljqys-rust-analyzer-2026-02-02' export nativeBuildInputs -out='/home/work/Documents/rust/invest-bot/outputs/out' +out='/home/mrfluffy/Documents/projects/rust/vibe-invest/outputs/out' export out outputBin='out' outputDev='out' @@ -173,7 +173,7 @@ preConfigurePhases=' updateAutotoolsGnuConfigScriptsPhase' declare -a preFixupHooks=('_moveToShare' '_multioutDocs' '_multioutDevs' ) preferLocalBuild='1' export preferLocalBuild -prefix='/home/work/Documents/rust/invest-bot/outputs/out' +prefix='/home/mrfluffy/Documents/projects/rust/vibe-invest/outputs/out' declare -a propagatedBuildDepFiles=('propagated-build-build-deps' 'propagated-native-build-inputs' 'propagated-build-target-deps' ) propagatedBuildInputs='' export propagatedBuildInputs diff --git a/src/bot.rs b/src/bot.rs index aff2ed8..e824220 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -115,6 +115,14 @@ impl TradingBot { Ok(bot) } + pub fn get_entry_atrs(&self) -> HashMap { + self.strategy.entry_atrs.clone() + } + + pub fn get_high_water_marks(&self) -> HashMap { + self.strategy.high_water_marks.clone() + } + // ── Persistence helpers ────────────────────────────────────────── fn load_json_map( diff --git a/src/dashboard.rs b/src/dashboard.rs index 94702eb..eda5e43 100644 --- a/src/dashboard.rs +++ b/src/dashboard.rs @@ -12,13 +12,23 @@ use std::path::Path; use std::sync::Arc; use tower_http::cors::CorsLayer; -use crate::alpaca::AlpacaClient; -use crate::paths::LIVE_EQUITY_FILE; -use crate::types::EquitySnapshot; +use crate::{ + alpaca::AlpacaClient, + config::{ATR_TRAIL_ACTIVATION_MULTIPLIER, ATR_TRAIL_MULTIPLIER}, + paths::{LIVE_ENTRY_ATRS_FILE, LIVE_EQUITY_FILE, LIVE_HIGH_WATER_MARKS_FILE}, + types::EquitySnapshot, +}; +use std::collections::HashMap; + +pub struct DashboardInitData { + pub entry_atrs: HashMap, + pub high_water_marks: HashMap, +} /// Shared state for the dashboard. pub struct DashboardState { pub client: AlpacaClient, + pub init_data: DashboardInitData, } #[derive(Serialize)] @@ -48,6 +58,8 @@ struct PositionResponse { unrealized_pnl: f64, pnl_pct: f64, change_today: f64, + trail_status: String, + stop_loss_price: f64, } #[derive(Serialize)] @@ -363,6 +375,8 @@ const HTML_TEMPLATE: &str = r#"
Current
${formatCurrency(pos.current_price)}
P&L
${formatCurrency(pos.unrealized_pnl, true)}
Today
${changeSign}${pos.change_today.toFixed(2)}%
+
Trail Status
${pos.trail_status}
+
Stop Loss
${formatCurrency(pos.stop_loss_price)}
`; }).join(''); @@ -548,20 +562,49 @@ async fn api_positions(State(state): State>) -> impl IntoRes Ok(positions) => { let mut result: Vec = positions .iter() - .map(|p| PositionResponse { - symbol: p.symbol.clone(), - qty: p.qty.parse().unwrap_or(0.0), - market_value: p.market_value.parse().unwrap_or(0.0), - avg_entry_price: p.avg_entry_price.parse().unwrap_or(0.0), - current_price: p.current_price.parse().unwrap_or(0.0), - unrealized_pnl: p.unrealized_pl.parse().unwrap_or(0.0), - pnl_pct: p.unrealized_plpc.parse::().unwrap_or(0.0) * 100.0, - change_today: p - .change_today - .as_ref() - .and_then(|s| s.parse::().ok()) - .unwrap_or(0.0) - * 100.0, + .map(|p| { + let entry_price = p.avg_entry_price.parse().unwrap_or(0.0); + let current_price = p.current_price.parse().unwrap_or(0.0); + let pnl_pct = if entry_price > 0.0 { + (current_price - entry_price) / entry_price + } else { + 0.0 + }; + + let entry_atr = state.init_data.entry_atrs.get(&p.symbol).copied().unwrap_or(0.0); + let high_water_mark = state.init_data.high_water_marks.get(&p.symbol).copied().unwrap_or(entry_price); + + let activation_gain = if entry_atr > 0.0 { + (ATR_TRAIL_ACTIVATION_MULTIPLIER * entry_atr) / entry_price + } else { + 0.0 + }; + + let (trail_status, stop_loss_price) = if pnl_pct >= activation_gain && entry_atr > 0.0 { + let trail_distance = ATR_TRAIL_MULTIPLIER * entry_atr; + let stop_price = high_water_mark - trail_distance; + ("Active".to_string(), stop_price) + } else { + ("Inactive".to_string(), entry_price - ATR_TRAIL_MULTIPLIER * entry_atr) + }; + + PositionResponse { + symbol: p.symbol.clone(), + qty: p.qty.parse().unwrap_or(0.0), + market_value: p.market_value.parse().unwrap_or(0.0), + avg_entry_price: entry_price, + current_price, + unrealized_pnl: p.unrealized_pl.parse().unwrap_or(0.0), + pnl_pct: p.unrealized_plpc.parse::().unwrap_or(0.0) * 100.0, + change_today: p + .change_today + .as_ref() + .and_then(|s| s.parse::().ok()) + .unwrap_or(0.0) + * 100.0, + trail_status, + stop_loss_price, + } }) .collect(); @@ -614,8 +657,12 @@ async fn api_orders(State(state): State>) -> impl IntoRespon } /// Start the dashboard web server. -pub async fn start_dashboard(client: AlpacaClient, port: u16) -> anyhow::Result<()> { - let state = Arc::new(DashboardState { client }); +pub async fn start_dashboard( + client: AlpacaClient, + port: u16, + init_data: DashboardInitData, +) -> anyhow::Result<()> { + let state = Arc::new(DashboardState { client, init_data }); let app = Router::new() .route("/", get(index)) diff --git a/src/main.rs b/src/main.rs index 59b1156..fc42617 100644 --- a/src/main.rs +++ b/src/main.rs @@ -232,17 +232,27 @@ async fn run_live_trading(api_key: String, api_secret: String, args: Args) -> Re .parse() .unwrap_or(5000); + // Create the bot first to load its state + let mut bot = TradingBot::new(api_key.clone(), api_secret.clone(), args.timeframe).await?; + // Create a separate client for the dashboard let dashboard_client = AlpacaClient::new(api_key.clone(), api_secret.clone())?; + // Extract data for the dashboard + let init_data = dashboard::DashboardInitData { + entry_atrs: bot.get_entry_atrs(), + high_water_marks: bot.get_high_water_marks(), + }; + // Spawn dashboard in background tokio::spawn(async move { - if let Err(e) = dashboard::start_dashboard(dashboard_client, dashboard_port).await { + if let Err(e) = + dashboard::start_dashboard(dashboard_client, dashboard_port, init_data).await + { tracing::error!("Dashboard error: {}", e); } }); - // Run the trading bot - let mut bot = TradingBot::new(api_key, api_secret, args.timeframe).await?; + // Now run the bot's main loop bot.run().await }