- Created portfolio.service.ts with API client functions - Created AllocationChart component (donut chart) - Created AllocationTable component (detailed positions) - Created RebalanceCard component (rebalancing recommendations) - Created GoalCard component (financial goal progress) - Created PortfolioDashboard page (main dashboard) - Created CreatePortfolio page (new portfolio form) - Created CreateGoal page (new goal form) - Updated App.tsx with portfolio routes Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
255 lines
6.9 KiB
TypeScript
255 lines
6.9 KiB
TypeScript
/**
|
|
* Portfolio Service
|
|
* Client for connecting to the Portfolio API
|
|
*/
|
|
|
|
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3080';
|
|
|
|
// ============================================================================
|
|
// Types
|
|
// ============================================================================
|
|
|
|
export type RiskProfile = 'conservative' | 'moderate' | 'aggressive';
|
|
|
|
export interface Portfolio {
|
|
id: string;
|
|
userId: string;
|
|
name: string;
|
|
riskProfile: RiskProfile;
|
|
allocations: PortfolioAllocation[];
|
|
totalValue: number;
|
|
totalCost: number;
|
|
unrealizedPnl: number;
|
|
unrealizedPnlPercent: number;
|
|
realizedPnl: number;
|
|
lastRebalanced: string | null;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
}
|
|
|
|
export interface PortfolioAllocation {
|
|
id: string;
|
|
portfolioId: string;
|
|
asset: string;
|
|
targetPercent: number;
|
|
currentPercent: number;
|
|
quantity: number;
|
|
value: number;
|
|
cost: number;
|
|
pnl: number;
|
|
pnlPercent: number;
|
|
deviation: number;
|
|
}
|
|
|
|
export interface PortfolioGoal {
|
|
id: string;
|
|
userId: string;
|
|
name: string;
|
|
targetAmount: number;
|
|
currentAmount: number;
|
|
targetDate: string;
|
|
monthlyContribution: number;
|
|
progress: number;
|
|
projectedCompletion: string | null;
|
|
status: 'on_track' | 'at_risk' | 'behind';
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
}
|
|
|
|
export interface RebalanceRecommendation {
|
|
asset: string;
|
|
currentPercent: number;
|
|
targetPercent: number;
|
|
action: 'buy' | 'sell' | 'hold';
|
|
amount: number;
|
|
amountUSD: number;
|
|
priority: 'high' | 'medium' | 'low';
|
|
}
|
|
|
|
export interface PortfolioStats {
|
|
totalValue: number;
|
|
dayChange: number;
|
|
dayChangePercent: number;
|
|
weekChange: number;
|
|
weekChangePercent: number;
|
|
monthChange: number;
|
|
monthChangePercent: number;
|
|
allTimeChange: number;
|
|
allTimeChangePercent: number;
|
|
bestPerformer: { asset: string; change: number };
|
|
worstPerformer: { asset: string; change: number };
|
|
}
|
|
|
|
export interface CreatePortfolioInput {
|
|
name: string;
|
|
riskProfile: RiskProfile;
|
|
initialValue?: number;
|
|
}
|
|
|
|
export interface CreateGoalInput {
|
|
name: string;
|
|
targetAmount: number;
|
|
targetDate: string;
|
|
monthlyContribution: number;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Portfolio API Functions
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Get user's portfolios
|
|
*/
|
|
export async function getUserPortfolios(): Promise<Portfolio[]> {
|
|
const response = await fetch(`${API_URL}/api/v1/portfolio`, {
|
|
credentials: 'include',
|
|
});
|
|
if (!response.ok) throw new Error('Failed to fetch portfolios');
|
|
const data = await response.json();
|
|
return data.data || data;
|
|
}
|
|
|
|
/**
|
|
* Get portfolio by ID
|
|
*/
|
|
export async function getPortfolio(portfolioId: string): Promise<Portfolio> {
|
|
const response = await fetch(`${API_URL}/api/v1/portfolio/${portfolioId}`, {
|
|
credentials: 'include',
|
|
});
|
|
if (!response.ok) throw new Error('Failed to fetch portfolio');
|
|
const data = await response.json();
|
|
return data.data || data;
|
|
}
|
|
|
|
/**
|
|
* Create a new portfolio
|
|
*/
|
|
export async function createPortfolio(input: CreatePortfolioInput): Promise<Portfolio> {
|
|
const response = await fetch(`${API_URL}/api/v1/portfolio`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
credentials: 'include',
|
|
body: JSON.stringify(input),
|
|
});
|
|
if (!response.ok) throw new Error('Failed to create portfolio');
|
|
const data = await response.json();
|
|
return data.data || data;
|
|
}
|
|
|
|
/**
|
|
* Update portfolio allocations
|
|
*/
|
|
export async function updateAllocations(
|
|
portfolioId: string,
|
|
allocations: { asset: string; targetPercent: number }[]
|
|
): Promise<Portfolio> {
|
|
const response = await fetch(`${API_URL}/api/v1/portfolio/${portfolioId}/allocations`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
credentials: 'include',
|
|
body: JSON.stringify({ allocations }),
|
|
});
|
|
if (!response.ok) throw new Error('Failed to update allocations');
|
|
const data = await response.json();
|
|
return data.data || data;
|
|
}
|
|
|
|
/**
|
|
* Get rebalancing recommendations
|
|
*/
|
|
export async function getRebalanceRecommendations(
|
|
portfolioId: string
|
|
): Promise<RebalanceRecommendation[]> {
|
|
const response = await fetch(`${API_URL}/api/v1/portfolio/${portfolioId}/rebalance`, {
|
|
credentials: 'include',
|
|
});
|
|
if (!response.ok) throw new Error('Failed to fetch recommendations');
|
|
const data = await response.json();
|
|
return data.data || data;
|
|
}
|
|
|
|
/**
|
|
* Execute rebalancing
|
|
*/
|
|
export async function executeRebalance(portfolioId: string): Promise<Portfolio> {
|
|
const response = await fetch(`${API_URL}/api/v1/portfolio/${portfolioId}/rebalance`, {
|
|
method: 'POST',
|
|
credentials: 'include',
|
|
});
|
|
if (!response.ok) throw new Error('Failed to execute rebalance');
|
|
const data = await response.json();
|
|
return data.data || data;
|
|
}
|
|
|
|
/**
|
|
* Get portfolio statistics
|
|
*/
|
|
export async function getPortfolioStats(portfolioId: string): Promise<PortfolioStats> {
|
|
const response = await fetch(`${API_URL}/api/v1/portfolio/${portfolioId}/stats`, {
|
|
credentials: 'include',
|
|
});
|
|
if (!response.ok) throw new Error('Failed to fetch stats');
|
|
const data = await response.json();
|
|
return data.data || data;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Goals API Functions
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Get user's goals
|
|
*/
|
|
export async function getUserGoals(): Promise<PortfolioGoal[]> {
|
|
const response = await fetch(`${API_URL}/api/v1/portfolio/goals`, {
|
|
credentials: 'include',
|
|
});
|
|
if (!response.ok) throw new Error('Failed to fetch goals');
|
|
const data = await response.json();
|
|
return data.data || data;
|
|
}
|
|
|
|
/**
|
|
* Create a new goal
|
|
*/
|
|
export async function createGoal(input: CreateGoalInput): Promise<PortfolioGoal> {
|
|
const response = await fetch(`${API_URL}/api/v1/portfolio/goals`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
credentials: 'include',
|
|
body: JSON.stringify(input),
|
|
});
|
|
if (!response.ok) throw new Error('Failed to create goal');
|
|
const data = await response.json();
|
|
return data.data || data;
|
|
}
|
|
|
|
/**
|
|
* Update goal progress
|
|
*/
|
|
export async function updateGoalProgress(
|
|
goalId: string,
|
|
currentAmount: number
|
|
): Promise<PortfolioGoal> {
|
|
const response = await fetch(`${API_URL}/api/v1/portfolio/goals/${goalId}/progress`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
credentials: 'include',
|
|
body: JSON.stringify({ currentAmount }),
|
|
});
|
|
if (!response.ok) throw new Error('Failed to update goal');
|
|
const data = await response.json();
|
|
return data.data || data;
|
|
}
|
|
|
|
/**
|
|
* Delete a goal
|
|
*/
|
|
export async function deleteGoal(goalId: string): Promise<void> {
|
|
const response = await fetch(`${API_URL}/api/v1/portfolio/goals/${goalId}`, {
|
|
method: 'DELETE',
|
|
credentials: 'include',
|
|
});
|
|
if (!response.ok) throw new Error('Failed to delete goal');
|
|
}
|