feat: add portfolio summary to simulation page
Shows real-time portfolio metrics: - Cash Balance - Position (quantity and value) - Entry Price / Current Price - Unrealized P&L - Total Value - P&L (absolute and percentage) Updates as simulation runs and trades are executed.
This commit is contained in:
@@ -118,6 +118,7 @@ export interface Simulation {
|
||||
signals: Signal[] | null;
|
||||
klines?: { time: number; close: number }[];
|
||||
trade_log?: TradeLogEntry[];
|
||||
portfolio?: Portfolio;
|
||||
current_candle_index?: number;
|
||||
total_candles?: number;
|
||||
candles_processed?: number;
|
||||
@@ -138,6 +139,15 @@ export interface TradeLogEntry {
|
||||
entry_price: number | null;
|
||||
}
|
||||
|
||||
export interface Portfolio {
|
||||
initial_balance: number;
|
||||
current_balance: number;
|
||||
position: number;
|
||||
position_token: string;
|
||||
entry_price: number;
|
||||
current_price: number;
|
||||
}
|
||||
|
||||
export interface Signal {
|
||||
id: string;
|
||||
bot_id: string;
|
||||
|
||||
139
src/frontend/src/lib/components/PortfolioSummary.svelte
Normal file
139
src/frontend/src/lib/components/PortfolioSummary.svelte
Normal file
@@ -0,0 +1,139 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
initialBalance?: number;
|
||||
currentBalance?: number;
|
||||
position?: number;
|
||||
positionToken?: string;
|
||||
entryPrice?: number;
|
||||
currentPrice?: number;
|
||||
}
|
||||
|
||||
let {
|
||||
initialBalance = 10000,
|
||||
currentBalance = 10000,
|
||||
position = 0,
|
||||
positionToken = '',
|
||||
entryPrice = 0,
|
||||
currentPrice = 0
|
||||
}: Props = $props();
|
||||
|
||||
// Calculate metrics
|
||||
let positionValue = $derived(position * currentPrice);
|
||||
let totalValue = $derived(currentBalance + positionValue);
|
||||
let pnl = $derived(totalValue - initialBalance);
|
||||
let pnlPercent = $derived((pnl / initialBalance) * 100);
|
||||
let unrealizedPnL = $derived(position > 0 && entryPrice > 0 ? (currentPrice - entryPrice) / entryPrice * 100 : 0);
|
||||
</script>
|
||||
|
||||
<div class="portfolio-summary">
|
||||
<div class="metric">
|
||||
<span class="label">Cash Balance</span>
|
||||
<span class="value">${currentBalance.toFixed(2)}</span>
|
||||
</div>
|
||||
|
||||
{#if position > 0}
|
||||
<div class="metric">
|
||||
<span class="label">Position ({positionToken || 'Token'})</span>
|
||||
<span class="value highlight">{position.toFixed(6)}</span>
|
||||
</div>
|
||||
|
||||
<div class="metric">
|
||||
<span class="label">Position Value</span>
|
||||
<span class="value">${positionValue.toFixed(2)}</span>
|
||||
</div>
|
||||
|
||||
<div class="metric">
|
||||
<span class="label">Entry Price</span>
|
||||
<span class="value">${entryPrice.toFixed(8)}</span>
|
||||
</div>
|
||||
|
||||
<div class="metric">
|
||||
<span class="label">Current Price</span>
|
||||
<span class="value">${currentPrice.toFixed(8)}</span>
|
||||
</div>
|
||||
|
||||
<div class="metric">
|
||||
<span class="label">Unrealized P&L</span>
|
||||
<span class="value" class:positive={unrealizedPnL > 0} class:negative={unrealizedPnL < 0}>
|
||||
{unrealizedPnL >= 0 ? '+' : ''}{unrealizedPnL.toFixed(2)}%
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<div class="metric total">
|
||||
<span class="label">Total Value</span>
|
||||
<span class="value">${totalValue.toFixed(2)}</span>
|
||||
</div>
|
||||
|
||||
<div class="metric">
|
||||
<span class="label">P&L</span>
|
||||
<span class="value large" class:positive={pnl > 0} class:negative={pnl < 0}>
|
||||
{pnl >= 0 ? '+' : ''}${pnl.toFixed(2)} ({pnlPercent >= 0 ? '+' : ''}{pnlPercent.toFixed(2)}%)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.portfolio-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.metric {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.metric .label {
|
||||
font-size: 0.75rem;
|
||||
color: #888;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.metric .value {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.metric .value.highlight {
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.metric .value.large {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.metric.total {
|
||||
grid-column: 1 / -1;
|
||||
padding-top: 0.5rem;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.metric.total .value {
|
||||
font-size: 1.5rem;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.positive {
|
||||
color: #22c55e !important;
|
||||
}
|
||||
|
||||
.negative {
|
||||
color: #ef4444 !important;
|
||||
}
|
||||
|
||||
.divider {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
@@ -4,6 +4,7 @@ export { default as BotSelector } from './BotSelector.svelte';
|
||||
export { default as StrategyPreview } from './StrategyPreview.svelte';
|
||||
export { default as SignalChart } from './SignalChart.svelte';
|
||||
export { default as TradeDashboard } from './TradeDashboard.svelte';
|
||||
export { default as PortfolioSummary } from './PortfolioSummary.svelte';
|
||||
export { default as BacktestChart } from './BacktestChart.svelte';
|
||||
export { default as ProUpgradeBanner } from './ProUpgradeBanner.svelte';
|
||||
export { default as TokenPicker } from './TokenPicker.svelte';
|
||||
|
||||
@@ -15,11 +15,21 @@ export interface TradeLogEntry {
|
||||
entry_price: number | null;
|
||||
}
|
||||
|
||||
export interface Portfolio {
|
||||
initial_balance: number;
|
||||
current_balance: number;
|
||||
position: number;
|
||||
position_token: string;
|
||||
entry_price: number;
|
||||
current_price: number;
|
||||
}
|
||||
|
||||
export interface SimulationState {
|
||||
currentSimulation: Simulation | null;
|
||||
signals: Signal[];
|
||||
klines: KlineData[];
|
||||
tradeLog: TradeLogEntry[];
|
||||
portfolio: Portfolio;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
@@ -29,6 +39,14 @@ const initialState: SimulationState = {
|
||||
signals: [],
|
||||
klines: [],
|
||||
tradeLog: [],
|
||||
portfolio: {
|
||||
initial_balance: 10000,
|
||||
current_balance: 10000,
|
||||
position: 0,
|
||||
position_token: '',
|
||||
entry_price: 0,
|
||||
current_price: 0
|
||||
},
|
||||
isLoading: false,
|
||||
error: null
|
||||
};
|
||||
@@ -40,7 +58,15 @@ export function setCurrentSimulation(simulation: Simulation | null) {
|
||||
...state,
|
||||
currentSimulation: simulation,
|
||||
klines: simulation?.klines || [],
|
||||
tradeLog: simulation?.trade_log || []
|
||||
tradeLog: simulation?.trade_log || [],
|
||||
portfolio: simulation?.portfolio || state.portfolio
|
||||
}));
|
||||
}
|
||||
|
||||
export function updatePortfolio(portfolio: Partial<Portfolio>) {
|
||||
simulationStore.update(state => ({
|
||||
...state,
|
||||
portfolio: { ...state.portfolio, ...portfolio }
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { isAuthenticated, isLoading, currentBotStore, setCurrentBot, simulationStore, setCurrentSimulation, addSignals, clearSignals, setSimulationLoading, setSimulationError } from '$lib/stores';
|
||||
import { api } from '$lib/api';
|
||||
import { SignalChart, TradeDashboard } from '$lib/components';
|
||||
import { SignalChart, TradeDashboard, PortfolioSummary } from '$lib/components';
|
||||
|
||||
let botId = $derived($page.params.id);
|
||||
let tokenName = $state('');
|
||||
@@ -155,7 +155,18 @@
|
||||
</section>
|
||||
|
||||
<section class="signals-section">
|
||||
<h2>Price Chart</h2>
|
||||
<h2>Portfolio</h2>
|
||||
|
||||
<PortfolioSummary
|
||||
initialBalance={$simulationStore.currentSimulation?.portfolio?.initial_balance || 10000}
|
||||
currentBalance={$simulationStore.currentSimulation?.portfolio?.current_balance || 10000}
|
||||
position={$simulationStore.currentSimulation?.portfolio?.position || 0}
|
||||
positionToken={$simulationStore.currentSimulation?.portfolio?.position_token || ''}
|
||||
entryPrice={$simulationStore.currentSimulation?.portfolio?.entry_price || 0}
|
||||
currentPrice={$simulationStore.currentSimulation?.portfolio?.current_price || 0}
|
||||
/>
|
||||
|
||||
<h2 style="margin-top: 1.5rem;">Price Chart</h2>
|
||||
|
||||
<SignalChart signals={$simulationStore.signals} klines={$simulationStore.currentSimulation?.klines || []} height={250} />
|
||||
|
||||
|
||||
Reference in New Issue
Block a user