189 lines
5.4 KiB
TypeScript
189 lines
5.4 KiB
TypeScript
import { create } from 'zustand';
|
|
import { invoke } from '../lib/tauri';
|
|
|
|
export interface PortfolioSummary {
|
|
totalValueUsd: number;
|
|
totalCostBasisUsd: number;
|
|
totalPnlUsd: number;
|
|
totalPnlPercent: number;
|
|
dayChangeUsd: number;
|
|
dayChangePercent: number;
|
|
}
|
|
|
|
export interface AssetHolding {
|
|
asset: string;
|
|
symbol: string;
|
|
balance: number;
|
|
balanceFormatted: string;
|
|
priceUsd: number;
|
|
valueUsd: number;
|
|
costBasisUsd: number;
|
|
pnlUsd: number;
|
|
pnlPercent: number;
|
|
allocationPercent: number;
|
|
}
|
|
|
|
export interface TaxableTransaction {
|
|
id: string;
|
|
txType: 'buy' | 'sell' | 'swap' | 'transfer';
|
|
asset: string;
|
|
amount: number;
|
|
priceUsd: number;
|
|
totalUsd: number;
|
|
costBasisUsd?: number;
|
|
gainLossUsd?: number;
|
|
timestamp: number;
|
|
isLongTerm: boolean;
|
|
}
|
|
|
|
export interface HistoryPoint {
|
|
timestamp: number;
|
|
valueUsd: number;
|
|
}
|
|
|
|
interface PortfolioState {
|
|
summary: PortfolioSummary | null;
|
|
holdings: AssetHolding[];
|
|
taxReport: TaxableTransaction[];
|
|
history: HistoryPoint[];
|
|
isLoading: boolean;
|
|
error: string | null;
|
|
}
|
|
|
|
interface PortfolioActions {
|
|
loadSummary: () => Promise<void>;
|
|
loadHoldings: () => Promise<void>;
|
|
loadTaxReport: (year: number) => Promise<void>;
|
|
exportTaxReport: (year: number, format: 'csv' | 'txf' | 'json') => Promise<string>;
|
|
loadHistory: (days: number) => Promise<void>;
|
|
clearError: () => void;
|
|
}
|
|
|
|
// Transform snake_case to camelCase
|
|
const transformSummary = (data: Record<string, unknown>): PortfolioSummary => ({
|
|
totalValueUsd: data.total_value_usd as number,
|
|
totalCostBasisUsd: data.total_cost_basis_usd as number,
|
|
totalPnlUsd: data.total_pnl_usd as number,
|
|
totalPnlPercent: data.total_pnl_percent as number,
|
|
dayChangeUsd: data.day_change_usd as number,
|
|
dayChangePercent: data.day_change_percent as number,
|
|
});
|
|
|
|
const transformHolding = (data: Record<string, unknown>): AssetHolding => ({
|
|
asset: data.asset as string,
|
|
symbol: data.symbol as string,
|
|
balance: data.balance as number,
|
|
balanceFormatted: data.balance_formatted as string,
|
|
priceUsd: data.price_usd as number,
|
|
valueUsd: data.value_usd as number,
|
|
costBasisUsd: data.cost_basis_usd as number,
|
|
pnlUsd: data.pnl_usd as number,
|
|
pnlPercent: data.pnl_percent as number,
|
|
allocationPercent: data.allocation_percent as number,
|
|
});
|
|
|
|
const transformTaxTx = (data: Record<string, unknown>): TaxableTransaction => ({
|
|
id: data.id as string,
|
|
txType: data.tx_type as TaxableTransaction['txType'],
|
|
asset: data.asset as string,
|
|
amount: data.amount as number,
|
|
priceUsd: data.price_usd as number,
|
|
totalUsd: data.total_usd as number,
|
|
costBasisUsd: data.cost_basis_usd as number | undefined,
|
|
gainLossUsd: data.gain_loss_usd as number | undefined,
|
|
timestamp: data.timestamp as number,
|
|
isLongTerm: data.is_long_term as boolean,
|
|
});
|
|
|
|
const transformHistory = (data: Record<string, unknown>): HistoryPoint => ({
|
|
timestamp: data.timestamp as number,
|
|
valueUsd: data.value_usd as number,
|
|
});
|
|
|
|
export const usePortfolioStore = create<PortfolioState & PortfolioActions>((set) => ({
|
|
summary: null,
|
|
holdings: [],
|
|
taxReport: [],
|
|
history: [],
|
|
isLoading: false,
|
|
error: null,
|
|
|
|
loadSummary: async () => {
|
|
try {
|
|
set({ isLoading: true, error: null });
|
|
const data = await invoke<Record<string, unknown>>('portfolio_get_summary');
|
|
const summary = transformSummary(data);
|
|
set({ summary, isLoading: false });
|
|
} catch (error) {
|
|
set({ error: String(error), isLoading: false });
|
|
}
|
|
},
|
|
|
|
loadHoldings: async () => {
|
|
try {
|
|
set({ isLoading: true, error: null });
|
|
const data = await invoke<Record<string, unknown>[]>('portfolio_get_holdings');
|
|
const holdings = data.map(transformHolding);
|
|
set({ holdings, isLoading: false });
|
|
} catch (error) {
|
|
set({ error: String(error), isLoading: false });
|
|
}
|
|
},
|
|
|
|
loadTaxReport: async (year: number) => {
|
|
try {
|
|
set({ isLoading: true, error: null });
|
|
const data = await invoke<Record<string, unknown>[]>('portfolio_get_tax_report', { year });
|
|
const taxReport = data.map(transformTaxTx);
|
|
set({ taxReport, isLoading: false });
|
|
} catch (error) {
|
|
set({ error: String(error), isLoading: false });
|
|
}
|
|
},
|
|
|
|
exportTaxReport: async (year: number, format: 'csv' | 'txf' | 'json') => {
|
|
try {
|
|
const data = await invoke<string>('portfolio_export_tax_report', { year, format });
|
|
return data;
|
|
} catch (error) {
|
|
set({ error: String(error) });
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
loadHistory: async (days: number) => {
|
|
try {
|
|
set({ isLoading: true, error: null });
|
|
const data = await invoke<Record<string, unknown>[]>('portfolio_get_history', { days });
|
|
const history = data.map(transformHistory);
|
|
set({ history, isLoading: false });
|
|
} catch (error) {
|
|
set({ error: String(error), isLoading: false });
|
|
}
|
|
},
|
|
|
|
clearError: () => set({ error: null }),
|
|
}));
|
|
|
|
// Helper to format currency
|
|
export const formatUSD = (value: number): string => {
|
|
return new Intl.NumberFormat('en-US', {
|
|
style: 'currency',
|
|
currency: 'USD',
|
|
}).format(value);
|
|
};
|
|
|
|
// Helper to format percentage
|
|
export const formatPercent = (value: number): string => {
|
|
const sign = value >= 0 ? '+' : '';
|
|
return `${sign}${value.toFixed(2)}%`;
|
|
};
|
|
|
|
// Alias for formatting USD
|
|
export const formatCurrency = formatUSD;
|
|
|
|
// Helper to format sompi to SYN
|
|
export const formatAmount = (sompi: number): string => {
|
|
const syn = sompi / 100_000_000;
|
|
return `${syn.toFixed(8)} SYN`;
|
|
};
|