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:
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
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 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';
|
||||||
|
|||||||
@@ -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 }
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user