atr tracking
This commit is contained in:
@@ -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<String, f64>,
|
||||
pub high_water_marks: HashMap<String, f64>,
|
||||
}
|
||||
|
||||
/// 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#"<!DOCTYPE html>
|
||||
<div class="position-detail"><div class="position-detail-label">Current</div><div class="position-detail-value">${formatCurrency(pos.current_price)}</div></div>
|
||||
<div class="position-detail"><div class="position-detail-label">P&L</div><div class="position-detail-value ${pnlClass}">${formatCurrency(pos.unrealized_pnl, true)}</div></div>
|
||||
<div class="position-detail"><div class="position-detail-label">Today</div><div class="position-detail-value ${changeClass}">${changeSign}${pos.change_today.toFixed(2)}%</div></div>
|
||||
<div class="position-detail"><div class="position-detail-label">Trail Status</div><div class="position-detail-value">${pos.trail_status}</div></div>
|
||||
<div class="position-detail"><div class="position-detail-label">Stop Loss</div><div class="position-detail-value">${formatCurrency(pos.stop_loss_price)}</div></div>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
@@ -548,20 +562,49 @@ async fn api_positions(State(state): State<Arc<DashboardState>>) -> impl IntoRes
|
||||
Ok(positions) => {
|
||||
let mut result: Vec<PositionResponse> = 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::<f64>().unwrap_or(0.0) * 100.0,
|
||||
change_today: p
|
||||
.change_today
|
||||
.as_ref()
|
||||
.and_then(|s| s.parse::<f64>().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::<f64>().unwrap_or(0.0) * 100.0,
|
||||
change_today: p
|
||||
.change_today
|
||||
.as_ref()
|
||||
.and_then(|s| s.parse::<f64>().ok())
|
||||
.unwrap_or(0.0)
|
||||
* 100.0,
|
||||
trail_status,
|
||||
stop_loss_price,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -614,8 +657,12 @@ async fn api_orders(State(state): State<Arc<DashboardState>>) -> 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))
|
||||
|
||||
Reference in New Issue
Block a user