transaction hystory
This commit is contained in:
159
src/dashboard.rs
159
src/dashboard.rs
@@ -50,6 +50,16 @@ struct PositionResponse {
|
||||
change_today: f64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct OrderHistoryResponse {
|
||||
symbol: String,
|
||||
side: String,
|
||||
qty: f64,
|
||||
filled_price: f64,
|
||||
filled_at: String,
|
||||
status: String,
|
||||
}
|
||||
|
||||
const HTML_TEMPLATE: &str = r#"<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
@@ -66,7 +76,7 @@ const HTML_TEMPLATE: &str = r#"<!DOCTYPE html>
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
.container { max-width: 1400px; margin: 0 auto; }
|
||||
.container { max-width: 1800px; margin: 0 auto; }
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
@@ -93,6 +103,15 @@ const HTML_TEMPLATE: &str = r#"<!DOCTYPE html>
|
||||
.stat-value { font-size: 1.8rem; font-weight: 700; }
|
||||
.stat-value.positive { color: #00ff88; }
|
||||
.stat-value.negative { color: #ff4757; }
|
||||
.main-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 380px;
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
@media (max-width: 1000px) {
|
||||
.main-layout { grid-template-columns: 1fr; }
|
||||
}
|
||||
.chart-container {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 16px;
|
||||
@@ -101,6 +120,49 @@ const HTML_TEMPLATE: &str = r#"<!DOCTYPE html>
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.chart-title { font-size: 1.3rem; margin-bottom: 20px; color: #fff; }
|
||||
.transactions-panel {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: calc(100vh - 220px);
|
||||
}
|
||||
.transactions-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(255,255,255,0.2) transparent;
|
||||
}
|
||||
.transactions-list::-webkit-scrollbar { width: 6px; }
|
||||
.transactions-list::-webkit-scrollbar-track { background: transparent; }
|
||||
.transactions-list::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); border-radius: 3px; }
|
||||
.tx-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.06);
|
||||
}
|
||||
.tx-row:last-child { border-bottom: none; }
|
||||
.tx-left { display: flex; align-items: center; gap: 10px; }
|
||||
.tx-side {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
text-transform: uppercase;
|
||||
min-width: 36px;
|
||||
text-align: center;
|
||||
}
|
||||
.tx-side.buy { background: rgba(0,255,136,0.2); color: #00ff88; }
|
||||
.tx-side.sell { background: rgba(255,71,87,0.2); color: #ff4757; }
|
||||
.tx-symbol { font-weight: 600; font-size: 0.95rem; }
|
||||
.tx-qty { color: #888; font-size: 0.8rem; }
|
||||
.tx-right { text-align: right; }
|
||||
.tx-price { font-size: 0.9rem; font-weight: 500; }
|
||||
.tx-time { font-size: 0.7rem; color: #666; margin-top: 2px; }
|
||||
.positions-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
@@ -170,16 +232,26 @@ const HTML_TEMPLATE: &str = r#"<!DOCTYPE html>
|
||||
<div class="stat-value" id="stat-positions">0</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<h2 class="chart-title">Portfolio Performance</h2>
|
||||
<canvas id="portfolioChart"></canvas>
|
||||
<div class="main-layout">
|
||||
<div class="left-column">
|
||||
<div class="chart-container">
|
||||
<h2 class="chart-title">Portfolio Performance</h2>
|
||||
<canvas id="portfolioChart"></canvas>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<h2 class="chart-title">Current Positions</h2>
|
||||
<div class="positions-grid" id="positions-grid"><div class="loading">Loading...</div></div>
|
||||
</div>
|
||||
<p class="last-updated" id="last-updated"></p>
|
||||
<p class="data-source" id="data-source"></p>
|
||||
</div>
|
||||
<div class="transactions-panel">
|
||||
<h2 class="chart-title">Transaction History</h2>
|
||||
<div class="transactions-list" id="transactions-list">
|
||||
<div class="loading">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<h2 class="chart-title">Current Positions</h2>
|
||||
<div class="positions-grid" id="positions-grid"><div class="loading">Loading...</div></div>
|
||||
</div>
|
||||
<p class="last-updated" id="last-updated"></p>
|
||||
<p class="data-source" id="data-source"></p>
|
||||
</div>
|
||||
<button class="refresh-btn" onclick="loadAllData()">Refresh</button>
|
||||
<script>
|
||||
@@ -297,12 +369,38 @@ const HTML_TEMPLATE: &str = r#"<!DOCTYPE html>
|
||||
} catch (error) { console.error('Error loading positions:', error); }
|
||||
}
|
||||
|
||||
async function loadOrders() {
|
||||
try {
|
||||
const response = await fetch('/api/orders');
|
||||
const orders = await response.json();
|
||||
const list = document.getElementById('transactions-list');
|
||||
if (orders.length === 0) {
|
||||
list.innerHTML = '<div class="loading">No transactions yet</div>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = orders.map(o => {
|
||||
const sideClass = o.side === 'buy' ? 'buy' : 'sell';
|
||||
return `<div class="tx-row">
|
||||
<div class="tx-left">
|
||||
<span class="tx-side ${sideClass}">${o.side}</span>
|
||||
<span class="tx-symbol">${o.symbol}</span>
|
||||
<span class="tx-qty">x${o.qty}</span>
|
||||
</div>
|
||||
<div class="tx-right">
|
||||
<div class="tx-price">${formatCurrency(o.filled_price)}</div>
|
||||
<div class="tx-time">${o.filled_at}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
} catch (error) { console.error('Error loading orders:', error); }
|
||||
}
|
||||
|
||||
function updateTimestamp() {
|
||||
document.getElementById('last-updated').textContent = 'Last updated: ' + new Date().toLocaleString();
|
||||
}
|
||||
|
||||
async function loadAllData() {
|
||||
await Promise.all([loadAccountStats(), loadEquityCurve(), loadPositions()]);
|
||||
await Promise.all([loadAccountStats(), loadEquityCurve(), loadPositions(), loadOrders()]);
|
||||
updateTimestamp();
|
||||
}
|
||||
|
||||
@@ -477,6 +575,44 @@ async fn api_positions(State(state): State<Arc<DashboardState>>) -> impl IntoRes
|
||||
}
|
||||
}
|
||||
|
||||
async fn api_orders(State(state): State<Arc<DashboardState>>) -> impl IntoResponse {
|
||||
match state.client.get_orders(100).await {
|
||||
Ok(orders) => {
|
||||
let result: Vec<OrderHistoryResponse> = orders
|
||||
.into_iter()
|
||||
.filter(|o| o.status == "filled")
|
||||
.map(|o| {
|
||||
let filled_at = o
|
||||
.filled_at
|
||||
.or(o.created_at)
|
||||
.unwrap_or_default();
|
||||
let display_time = if filled_at.len() >= 16 {
|
||||
filled_at[..16].replace("T", " ")
|
||||
} else {
|
||||
filled_at
|
||||
};
|
||||
OrderHistoryResponse {
|
||||
symbol: o.symbol,
|
||||
side: o.side,
|
||||
qty: o.qty.parse().unwrap_or(0.0),
|
||||
filled_price: o
|
||||
.filled_avg_price
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(0.0),
|
||||
filled_at: display_time,
|
||||
status: o.status,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
Json(result).into_response()
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to get orders: {}", e);
|
||||
Json(Vec::<OrderHistoryResponse>::new()).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Start the dashboard web server.
|
||||
pub async fn start_dashboard(client: AlpacaClient, port: u16) -> anyhow::Result<()> {
|
||||
let state = Arc::new(DashboardState { client });
|
||||
@@ -486,6 +622,7 @@ pub async fn start_dashboard(client: AlpacaClient, port: u16) -> anyhow::Result<
|
||||
.route("/api/account", get(api_account))
|
||||
.route("/api/equity", get(api_equity))
|
||||
.route("/api/positions", get(api_positions))
|
||||
.route("/api/orders", get(api_orders))
|
||||
.layer(CorsLayer::permissive())
|
||||
.with_state(state);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user