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:
shokollm
2026-04-12 07:15:11 +00:00
parent 3013326ded
commit bba773251a
7 changed files with 201 additions and 3 deletions

View File

@@ -60,6 +60,9 @@ def run_simulation_sync(
for k in engine.klines for k in engine.klines
] ]
simulation.trade_log = engine.trade_log simulation.trade_log = engine.trade_log
# Save portfolio data
if hasattr(engine, 'current_balance') and engine.current_balance is not None:
simulation.signals = [serialize_signal(s) for s in engine.signals]
db.commit() db.commit()
finally: finally:
db.close() db.close()

View File

@@ -169,6 +169,14 @@ class SimulateEngine:
self.results["total_candles"] = self.total_candles self.results["total_candles"] = self.total_candles
self.results["klines"] = self.klines # Include klines for chart display self.results["klines"] = self.klines # Include klines for chart display
self.results["trade_log"] = self.trade_log # Include trade log for dashboard self.results["trade_log"] = self.trade_log # Include trade log for dashboard
self.results["portfolio"] = {
"initial_balance": self.config.get("initial_balance", 10000),
"current_balance": self.current_balance,
"position": self.position,
"position_token": self.position_token,
"entry_price": self.entry_price,
"current_price": self.last_close,
}
self.results["started_at"] = self.started_at self.results["started_at"] = self.started_at
self.results["ended_at"] = datetime.utcnow() self.results["ended_at"] = datetime.utcnow()

View File

@@ -118,6 +118,7 @@ export interface Simulation {
signals: Signal[] | null; signals: Signal[] | null;
klines?: { time: number; close: number }[]; klines?: { time: number; close: number }[];
trade_log?: TradeLogEntry[]; trade_log?: TradeLogEntry[];
portfolio?: Portfolio;
current_candle_index?: number; current_candle_index?: number;
total_candles?: number; total_candles?: number;
candles_processed?: number; candles_processed?: number;
@@ -138,6 +139,15 @@ export interface TradeLogEntry {
entry_price: number | null; 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 { export interface Signal {
id: string; id: string;
bot_id: string; bot_id: string;

View 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>

View File

@@ -4,6 +4,7 @@ export { default as BotSelector } from './BotSelector.svelte';
export { default as StrategyPreview } from './StrategyPreview.svelte'; export { default as StrategyPreview } from './StrategyPreview.svelte';
export { default as SignalChart } from './SignalChart.svelte'; export { default as SignalChart } from './SignalChart.svelte';
export { default as TradeDashboard } from './TradeDashboard.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 BacktestChart } from './BacktestChart.svelte';
export { default as ProUpgradeBanner } from './ProUpgradeBanner.svelte'; export { default as ProUpgradeBanner } from './ProUpgradeBanner.svelte';
export { default as TokenPicker } from './TokenPicker.svelte'; export { default as TokenPicker } from './TokenPicker.svelte';

View File

@@ -15,11 +15,21 @@ export interface TradeLogEntry {
entry_price: number | null; 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 { export interface SimulationState {
currentSimulation: Simulation | null; currentSimulation: Simulation | null;
signals: Signal[]; signals: Signal[];
klines: KlineData[]; klines: KlineData[];
tradeLog: TradeLogEntry[]; tradeLog: TradeLogEntry[];
portfolio: Portfolio;
isLoading: boolean; isLoading: boolean;
error: string | null; error: string | null;
} }
@@ -29,6 +39,14 @@ const initialState: SimulationState = {
signals: [], signals: [],
klines: [], klines: [],
tradeLog: [], tradeLog: [],
portfolio: {
initial_balance: 10000,
current_balance: 10000,
position: 0,
position_token: '',
entry_price: 0,
current_price: 0
},
isLoading: false, isLoading: false,
error: null error: null
}; };
@@ -40,7 +58,15 @@ export function setCurrentSimulation(simulation: Simulation | null) {
...state, ...state,
currentSimulation: simulation, currentSimulation: simulation,
klines: simulation?.klines || [], 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 }
})); }));
} }

View File

@@ -4,7 +4,7 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { isAuthenticated, isLoading, currentBotStore, setCurrentBot, simulationStore, setCurrentSimulation, addSignals, clearSignals, setSimulationLoading, setSimulationError } from '$lib/stores'; import { isAuthenticated, isLoading, currentBotStore, setCurrentBot, simulationStore, setCurrentSimulation, addSignals, clearSignals, setSimulationLoading, setSimulationError } from '$lib/stores';
import { api } from '$lib/api'; 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 botId = $derived($page.params.id);
let tokenName = $state(''); let tokenName = $state('');
@@ -155,7 +155,18 @@
</section> </section>
<section class="signals-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} /> <SignalChart signals={$simulationStore.signals} klines={$simulationStore.currentSimulation?.klines || []} height={250} />