Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 42 additions & 15 deletions client/src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import AccountSelector from './components/AccountSelector';
import SummaryMetrics from './components/SummaryMetrics';
import SummaryMetrics, { resolveScopedAmount } from './components/SummaryMetrics';
import PositionsTable from './components/PositionsTable';
import { getSummary, getQqqTemperature } from './api/questrade';
import usePersistentState from './hooks/usePersistentState';
Expand Down Expand Up @@ -223,7 +223,10 @@ function buildClipboardSummary({
lines.push(`Total amount: ${formatMoney(totalAmount)}`);
lines.push(`Today's P&L: ${formatSignedMoney(pnl?.dayPnl)}`);
lines.push(`Open P&L: ${formatSignedMoney(pnl?.openPnl)}`);
lines.push(`Total P&L: ${formatSignedMoney(pnl?.totalPnl)}`);
const totalPnlAmount = resolveScopedAmount(pnl?.totalPnlBreakdown, currencyOption) ?? pnl?.totalPnl ?? null;
lines.push(`Total P&L: ${formatSignedMoney(totalPnlAmount)}`);
const netDepositAmount = resolveScopedAmount(pnl?.netDeposits, currencyOption);
lines.push(`Net deposits: ${formatSignedMoney(netDepositAmount ?? null)}`);
lines.push(`Total equity: ${formatMoney(balances?.totalEquity)}`);
lines.push(`Market value: ${formatMoney(balances?.marketValue)}`);
lines.push(`Cash: ${formatMoney(balances?.cash)}`);
Expand Down Expand Up @@ -431,7 +434,14 @@ function resolveDisplayTotalEquity(balances) {
return null;
}

const ZERO_PNL = Object.freeze({ dayPnl: 0, openPnl: 0, totalPnl: 0 });
const ZERO_PNL = Object.freeze({
dayPnl: 0,
openPnl: 0,
totalPnl: 0,
totalPnlBreakdown: null,
totalEquityBreakdown: null,
netDeposits: null,
});

function isFiniteNumber(value) {
return typeof value === 'number' && Number.isFinite(value);
Expand Down Expand Up @@ -1234,27 +1244,44 @@ export default function App() {
return positionPnlSummaries.perCurrency[activeCurrency.currency] || ZERO_PNL;
}, [activeCurrency, positionPnlSummaries, currencyRates, baseCurrency]);

const rawServerPnl = data?.pnl || null;

const activePnl = useMemo(() => {
if (!activeCurrency) {
return ZERO_PNL;
return rawServerPnl ? Object.assign({}, ZERO_PNL, rawServerPnl) : ZERO_PNL;
}

const balanceEntry = balancePnlSummaries[activeCurrency.scope]?.[activeCurrency.currency] || null;
const totalFromBalance = balanceEntry ? balanceEntry.totalPnl : null;
const hasBalanceTotal = isFiniteNumber(totalFromBalance);

const base = rawServerPnl ? Object.assign({}, ZERO_PNL, rawServerPnl) : { ...ZERO_PNL };

if (!balanceEntry) {
return {
dayPnl: fallbackPnl.dayPnl,
openPnl: fallbackPnl.openPnl,
totalPnl: null,
};
base.dayPnl = fallbackPnl.dayPnl;
base.openPnl = fallbackPnl.openPnl;
base.totalPnl = hasBalanceTotal ? totalFromBalance : null;
if (base.totalPnl === null && isFiniteNumber(fallbackPnl.totalPnl)) {
base.totalPnl = fallbackPnl.totalPnl;
}
return base;
}
return {
dayPnl: balanceEntry.dayPnl ?? fallbackPnl.dayPnl,
openPnl: balanceEntry.openPnl ?? fallbackPnl.openPnl,
totalPnl: hasBalanceTotal ? totalFromBalance : null,
};
}, [activeCurrency, balancePnlSummaries, fallbackPnl]);

const resolvedDay = balanceEntry.dayPnl ?? fallbackPnl.dayPnl;
const resolvedOpen = balanceEntry.openPnl ?? fallbackPnl.openPnl;
base.dayPnl = isFiniteNumber(resolvedDay) ? resolvedDay : fallbackPnl.dayPnl;
base.openPnl = isFiniteNumber(resolvedOpen) ? resolvedOpen : fallbackPnl.openPnl;

if (hasBalanceTotal) {
base.totalPnl = totalFromBalance;
} else if (isFiniteNumber(fallbackPnl.totalPnl)) {
base.totalPnl = fallbackPnl.totalPnl;
} else {
base.totalPnl = null;
}

return base;
}, [activeCurrency, balancePnlSummaries, fallbackPnl, rawServerPnl]);

const heatmapMarketValue = useMemo(() => {
if (activeBalances && typeof activeBalances === 'object') {
Expand Down
67 changes: 65 additions & 2 deletions client/src/components/SummaryMetrics.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,50 @@ import {
formatSignedPercent,
} from '../utils/formatters';

export function resolveScopedAmount(bucket, currencyOption) {
if (!bucket || typeof bucket !== 'object' || !currencyOption) {
return null;
}

const scope = currencyOption.scope || 'combined';
const currency = typeof currencyOption.currency === 'string' ? currencyOption.currency.toUpperCase() : null;
if (!currency) {
return null;
}

const sourceBucket =
scope === 'perCurrency'
? bucket.perCurrency || bucket.currency || bucket
: bucket.combined || bucket.currency || bucket.perCurrency || bucket;

if (!sourceBucket || typeof sourceBucket !== 'object') {
return null;
}

if (Object.prototype.hasOwnProperty.call(sourceBucket, currency)) {
const value = sourceBucket[currency];
if (typeof value === 'number' && Number.isFinite(value)) {
return value;
}
if (value === 0) {
return 0;
}
}

for (const [key, value] of Object.entries(sourceBucket)) {
if (typeof key === 'string' && key.toUpperCase() === currency) {
if (typeof value === 'number' && Number.isFinite(value)) {
return value;
}
if (value === 0) {
return 0;
}
}
}

return null;
}

function MetricRow({ label, value, extra, tone, className, onActivate }) {
const rowClass = className ? `equity-card__metric-row ${className}` : 'equity-card__metric-row';
const interactive = typeof onActivate === 'function';
Expand Down Expand Up @@ -212,13 +256,18 @@ export default function SummaryMetrics({
const cash = balances?.cash ?? null;
const buyingPower = balances?.buyingPower ?? null;

const scopedTotalPnl = resolveScopedAmount(pnl?.totalPnlBreakdown, currencyOption);
const totalPnlValue = scopedTotalPnl ?? pnl?.totalPnl ?? null;

const todayTone = classifyPnL(pnl?.dayPnl);
const openTone = classifyPnL(pnl?.openPnl);
const totalTone = classifyPnL(pnl?.totalPnl);
const totalTone = classifyPnL(totalPnlValue);

const formattedToday = formatSignedMoney(pnl?.dayPnl ?? null);
const formattedOpen = formatSignedMoney(pnl?.openPnl ?? null);
const formattedTotal = formatSignedMoney(pnl?.totalPnl ?? null);
const formattedTotal = formatSignedMoney(totalPnlValue ?? null);
const netDepositAmount = resolveScopedAmount(pnl?.netDeposits, currencyOption);
const formattedNetDeposits = formatSignedMoney(netDepositAmount ?? null);

const safeTotalEquity = Number.isFinite(totalEquity) ? totalEquity : null;

Expand Down Expand Up @@ -356,6 +405,7 @@ export default function SummaryMetrics({
onActivate={onShowPnlBreakdown ? () => onShowPnlBreakdown('open') : null}
/>
<MetricRow label="Total P&L" value={formattedTotal} tone={totalTone} />
<MetricRow label="Net deposits" value={formattedNetDeposits} tone="neutral" />
</dl>
<dl className="equity-card__metric-column">
<MetricRow label="Total equity" value={formatMoney(totalEquity)} tone="neutral" />
Expand Down Expand Up @@ -395,6 +445,19 @@ SummaryMetrics.propTypes = {
dayPnl: PropTypes.number,
openPnl: PropTypes.number,
totalPnl: PropTypes.number,
totalPnlBreakdown: PropTypes.shape({
combined: PropTypes.objectOf(PropTypes.number),
perCurrency: PropTypes.objectOf(PropTypes.number),
}),
totalEquityBreakdown: PropTypes.shape({
combined: PropTypes.objectOf(PropTypes.number),
perCurrency: PropTypes.objectOf(PropTypes.number),
}),
netDeposits: PropTypes.shape({
combined: PropTypes.objectOf(PropTypes.number),
perCurrency: PropTypes.objectOf(PropTypes.number),
counts: PropTypes.objectOf(PropTypes.number),
}),
}).isRequired,
asOf: PropTypes.string,
onRefresh: PropTypes.func,
Expand Down
Loading