feat: Add new services and stores for trading platform
Services: - alerts.service.ts: Alert management - currency.service.ts: Currency conversion - risk.service.ts: Risk calculation Stores: - investmentStore.ts: Investment state management - llmStore.ts: LLM integration state - mlStore.ts: ML model state - riskStore.ts: Risk metrics state Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
5779c9a5cf
commit
6d0673a799
384
src/services/alerts.service.ts
Normal file
384
src/services/alerts.service.ts
Normal file
@ -0,0 +1,384 @@
|
|||||||
|
/**
|
||||||
|
* Alerts Service
|
||||||
|
* API client for price alerts management
|
||||||
|
*
|
||||||
|
* Backend endpoints: /api/v1/trading/alerts/*
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { apiClient as api } from '../lib/apiClient';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Types
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alert condition types
|
||||||
|
* - above: Price goes above target
|
||||||
|
* - below: Price goes below target
|
||||||
|
* - crosses_above: Price crosses above target (from below)
|
||||||
|
* - crosses_below: Price crosses below target (from above)
|
||||||
|
*/
|
||||||
|
export type AlertCondition = 'above' | 'below' | 'crosses_above' | 'crosses_below';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alert status for filtering
|
||||||
|
*/
|
||||||
|
export type AlertStatus = 'active' | 'triggered' | 'disabled' | 'expired';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notification channels
|
||||||
|
*/
|
||||||
|
export type NotificationChannel = 'email' | 'push' | 'sms' | 'webhook';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Price Alert entity
|
||||||
|
*/
|
||||||
|
export interface PriceAlert {
|
||||||
|
id: string;
|
||||||
|
userId: string;
|
||||||
|
symbol: string;
|
||||||
|
condition: AlertCondition;
|
||||||
|
price: number;
|
||||||
|
note?: string;
|
||||||
|
isActive: boolean;
|
||||||
|
triggeredAt?: string;
|
||||||
|
triggeredPrice?: number;
|
||||||
|
notifyEmail: boolean;
|
||||||
|
notifyPush: boolean;
|
||||||
|
isRecurring: boolean;
|
||||||
|
expiresAt?: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Input for creating a new alert
|
||||||
|
*/
|
||||||
|
export interface CreateAlertInput {
|
||||||
|
symbol: string;
|
||||||
|
condition: AlertCondition;
|
||||||
|
price: number;
|
||||||
|
note?: string;
|
||||||
|
notifyEmail?: boolean;
|
||||||
|
notifyPush?: boolean;
|
||||||
|
isRecurring?: boolean;
|
||||||
|
expiresAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Input for updating an existing alert
|
||||||
|
*/
|
||||||
|
export interface UpdateAlertInput {
|
||||||
|
price?: number;
|
||||||
|
note?: string;
|
||||||
|
notifyEmail?: boolean;
|
||||||
|
notifyPush?: boolean;
|
||||||
|
isRecurring?: boolean;
|
||||||
|
isActive?: boolean;
|
||||||
|
expiresAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter options for listing alerts
|
||||||
|
*/
|
||||||
|
export interface AlertsFilter {
|
||||||
|
isActive?: boolean;
|
||||||
|
symbol?: string;
|
||||||
|
condition?: AlertCondition;
|
||||||
|
status?: AlertStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alert statistics
|
||||||
|
*/
|
||||||
|
export interface AlertStats {
|
||||||
|
total: number;
|
||||||
|
active: number;
|
||||||
|
triggered: number;
|
||||||
|
disabled: number;
|
||||||
|
expired: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API response wrapper
|
||||||
|
*/
|
||||||
|
interface ApiResponse<T> {
|
||||||
|
success: boolean;
|
||||||
|
data: T;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Alert CRUD Operations
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user's price alerts with optional filtering
|
||||||
|
*
|
||||||
|
* @param filter - Optional filter criteria
|
||||||
|
* @returns List of price alerts
|
||||||
|
*/
|
||||||
|
export async function getAlerts(filter: AlertsFilter = {}): Promise<PriceAlert[]> {
|
||||||
|
try {
|
||||||
|
const params: Record<string, string> = {};
|
||||||
|
if (filter.isActive !== undefined) params.isActive = String(filter.isActive);
|
||||||
|
if (filter.symbol) params.symbol = filter.symbol;
|
||||||
|
if (filter.condition) params.condition = filter.condition;
|
||||||
|
if (filter.status) params.status = filter.status;
|
||||||
|
|
||||||
|
const response = await api.get<ApiResponse<PriceAlert[]>>('/trading/alerts', { params });
|
||||||
|
return response.data.data || response.data as unknown as PriceAlert[];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch alerts:', error);
|
||||||
|
throw new Error('Failed to fetch alerts');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a specific alert by ID
|
||||||
|
*
|
||||||
|
* @param alertId - The alert ID
|
||||||
|
* @returns The price alert
|
||||||
|
*/
|
||||||
|
export async function getAlert(alertId: string): Promise<PriceAlert> {
|
||||||
|
try {
|
||||||
|
const response = await api.get<ApiResponse<PriceAlert>>(`/trading/alerts/${alertId}`);
|
||||||
|
return response.data.data || response.data as unknown as PriceAlert;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch alert:', error);
|
||||||
|
throw new Error('Failed to fetch alert');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new price alert
|
||||||
|
*
|
||||||
|
* @param input - Alert creation data
|
||||||
|
* @returns The created price alert
|
||||||
|
*/
|
||||||
|
export async function createAlert(input: CreateAlertInput): Promise<PriceAlert> {
|
||||||
|
try {
|
||||||
|
const response = await api.post<ApiResponse<PriceAlert>>('/trading/alerts', input);
|
||||||
|
return response.data.data || response.data as unknown as PriceAlert;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to create alert:', error);
|
||||||
|
throw new Error('Failed to create alert');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an existing alert
|
||||||
|
*
|
||||||
|
* @param alertId - The alert ID to update
|
||||||
|
* @param data - Fields to update
|
||||||
|
* @returns The updated price alert
|
||||||
|
*/
|
||||||
|
export async function updateAlert(alertId: string, data: UpdateAlertInput): Promise<PriceAlert> {
|
||||||
|
try {
|
||||||
|
const response = await api.patch<ApiResponse<PriceAlert>>(`/trading/alerts/${alertId}`, data);
|
||||||
|
return response.data.data || response.data as unknown as PriceAlert;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update alert:', error);
|
||||||
|
throw new Error('Failed to update alert');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an alert
|
||||||
|
*
|
||||||
|
* @param alertId - The alert ID to delete
|
||||||
|
*/
|
||||||
|
export async function deleteAlert(alertId: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
await api.delete(`/trading/alerts/${alertId}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to delete alert:', error);
|
||||||
|
throw new Error('Failed to delete alert');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Alert Enable/Disable Operations
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable an alert (set isActive to true)
|
||||||
|
*
|
||||||
|
* @param alertId - The alert ID
|
||||||
|
* @returns The updated price alert
|
||||||
|
*/
|
||||||
|
export async function enableAlert(alertId: string): Promise<PriceAlert> {
|
||||||
|
try {
|
||||||
|
const response = await api.post<ApiResponse<PriceAlert>>(`/trading/alerts/${alertId}/enable`);
|
||||||
|
return response.data.data || response.data as unknown as PriceAlert;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to enable alert:', error);
|
||||||
|
throw new Error('Failed to enable alert');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable an alert (set isActive to false)
|
||||||
|
*
|
||||||
|
* @param alertId - The alert ID
|
||||||
|
* @returns The updated price alert
|
||||||
|
*/
|
||||||
|
export async function disableAlert(alertId: string): Promise<PriceAlert> {
|
||||||
|
try {
|
||||||
|
const response = await api.post<ApiResponse<PriceAlert>>(`/trading/alerts/${alertId}/disable`);
|
||||||
|
return response.data.data || response.data as unknown as PriceAlert;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to disable alert:', error);
|
||||||
|
throw new Error('Failed to disable alert');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle alert enabled/disabled status
|
||||||
|
*
|
||||||
|
* @param alertId - The alert ID
|
||||||
|
* @param enabled - Whether to enable or disable
|
||||||
|
* @returns The updated price alert
|
||||||
|
*/
|
||||||
|
export async function toggleAlert(alertId: string, enabled: boolean): Promise<PriceAlert> {
|
||||||
|
return enabled ? enableAlert(alertId) : disableAlert(alertId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Alert Statistics
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get alert statistics for the current user
|
||||||
|
*
|
||||||
|
* @returns Alert statistics (total, active, triggered counts)
|
||||||
|
*/
|
||||||
|
export async function getAlertStats(): Promise<AlertStats> {
|
||||||
|
try {
|
||||||
|
const response = await api.get<ApiResponse<AlertStats>>('/trading/alerts/stats');
|
||||||
|
return response.data.data || response.data as unknown as AlertStats;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch alert stats:', error);
|
||||||
|
throw new Error('Failed to fetch alert stats');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Convenience Functions
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all active alerts
|
||||||
|
*
|
||||||
|
* @returns List of active price alerts
|
||||||
|
*/
|
||||||
|
export async function getActiveAlerts(): Promise<PriceAlert[]> {
|
||||||
|
return getAlerts({ isActive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get alerts for a specific symbol
|
||||||
|
*
|
||||||
|
* @param symbol - Trading symbol (e.g., 'BTCUSDT')
|
||||||
|
* @returns List of alerts for the symbol
|
||||||
|
*/
|
||||||
|
export async function getAlertsBySymbol(symbol: string): Promise<PriceAlert[]> {
|
||||||
|
return getAlerts({ symbol });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a price above alert
|
||||||
|
*
|
||||||
|
* @param symbol - Trading symbol
|
||||||
|
* @param price - Target price
|
||||||
|
* @param options - Additional options
|
||||||
|
* @returns The created alert
|
||||||
|
*/
|
||||||
|
export async function createPriceAboveAlert(
|
||||||
|
symbol: string,
|
||||||
|
price: number,
|
||||||
|
options: Omit<CreateAlertInput, 'symbol' | 'condition' | 'price'> = {}
|
||||||
|
): Promise<PriceAlert> {
|
||||||
|
return createAlert({
|
||||||
|
symbol,
|
||||||
|
condition: 'above',
|
||||||
|
price,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a price below alert
|
||||||
|
*
|
||||||
|
* @param symbol - Trading symbol
|
||||||
|
* @param price - Target price
|
||||||
|
* @param options - Additional options
|
||||||
|
* @returns The created alert
|
||||||
|
*/
|
||||||
|
export async function createPriceBelowAlert(
|
||||||
|
symbol: string,
|
||||||
|
price: number,
|
||||||
|
options: Omit<CreateAlertInput, 'symbol' | 'condition' | 'price'> = {}
|
||||||
|
): Promise<PriceAlert> {
|
||||||
|
return createAlert({
|
||||||
|
symbol,
|
||||||
|
condition: 'below',
|
||||||
|
price,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete all alerts for a specific symbol
|
||||||
|
*
|
||||||
|
* @param symbol - Trading symbol
|
||||||
|
*/
|
||||||
|
export async function deleteAlertsBySymbol(symbol: string): Promise<void> {
|
||||||
|
const alerts = await getAlertsBySymbol(symbol);
|
||||||
|
await Promise.all(alerts.map(alert => deleteAlert(alert.id)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable all alerts for a specific symbol
|
||||||
|
*
|
||||||
|
* @param symbol - Trading symbol
|
||||||
|
*/
|
||||||
|
export async function disableAlertsBySymbol(symbol: string): Promise<void> {
|
||||||
|
const alerts = await getAlertsBySymbol(symbol);
|
||||||
|
await Promise.all(
|
||||||
|
alerts
|
||||||
|
.filter(alert => alert.isActive)
|
||||||
|
.map(alert => disableAlert(alert.id))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Export
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export const alertsService = {
|
||||||
|
// CRUD
|
||||||
|
getAlerts,
|
||||||
|
getAlert,
|
||||||
|
createAlert,
|
||||||
|
updateAlert,
|
||||||
|
deleteAlert,
|
||||||
|
|
||||||
|
// Enable/Disable
|
||||||
|
enableAlert,
|
||||||
|
disableAlert,
|
||||||
|
toggleAlert,
|
||||||
|
|
||||||
|
// Statistics
|
||||||
|
getAlertStats,
|
||||||
|
|
||||||
|
// Convenience
|
||||||
|
getActiveAlerts,
|
||||||
|
getAlertsBySymbol,
|
||||||
|
createPriceAboveAlert,
|
||||||
|
createPriceBelowAlert,
|
||||||
|
deleteAlertsBySymbol,
|
||||||
|
disableAlertsBySymbol,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default alertsService;
|
||||||
150
src/services/currency.service.ts
Normal file
150
src/services/currency.service.ts
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
/**
|
||||||
|
* Currency Service
|
||||||
|
* API client for currency exchange endpoints
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { apiClient } from '../lib/apiClient';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Types
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export interface ExchangeRate {
|
||||||
|
fromCurrency: string;
|
||||||
|
toCurrency: string;
|
||||||
|
rate: number;
|
||||||
|
source: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConversionRequest {
|
||||||
|
amount: number;
|
||||||
|
fromCurrency: string;
|
||||||
|
toCurrency: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConversionResult {
|
||||||
|
originalAmount: number;
|
||||||
|
convertedAmount: number;
|
||||||
|
rate: number;
|
||||||
|
fromCurrency: string;
|
||||||
|
toCurrency: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateRateRequest {
|
||||||
|
fromCurrency: string;
|
||||||
|
toCurrency: string;
|
||||||
|
rate: number;
|
||||||
|
source?: string;
|
||||||
|
provider?: string;
|
||||||
|
metadata?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// API Client
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const api = apiClient;
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Currency Exchange API
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get exchange rate between two currencies
|
||||||
|
*/
|
||||||
|
export async function getRate(from: string, to: string): Promise<ExchangeRate> {
|
||||||
|
try {
|
||||||
|
const response = await api.get(`/currency/rates/${from}/${to}`);
|
||||||
|
return response.data.data || response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch exchange rate:', error);
|
||||||
|
throw new Error('Failed to fetch exchange rate');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all exchange rates for a base currency
|
||||||
|
*/
|
||||||
|
export async function getExchangeRates(baseCurrency: string): Promise<ExchangeRate[]> {
|
||||||
|
try {
|
||||||
|
const response = await api.get(`/currency/rates/${baseCurrency}`);
|
||||||
|
return response.data.data || response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch exchange rates:', error);
|
||||||
|
throw new Error('Failed to fetch exchange rates');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert amount between currencies
|
||||||
|
*/
|
||||||
|
export async function convert(
|
||||||
|
amount: number,
|
||||||
|
from: string,
|
||||||
|
to: string
|
||||||
|
): Promise<ConversionResult> {
|
||||||
|
try {
|
||||||
|
const response = await api.post('/currency/convert', {
|
||||||
|
amount,
|
||||||
|
fromCurrency: from,
|
||||||
|
toCurrency: to,
|
||||||
|
});
|
||||||
|
return response.data.data || response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to convert currency:', error);
|
||||||
|
throw new Error('Failed to convert currency');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of supported currencies
|
||||||
|
* Extracts unique currencies from exchange rates
|
||||||
|
*/
|
||||||
|
export async function getSupportedCurrencies(baseCurrency: string = 'USD'): Promise<string[]> {
|
||||||
|
try {
|
||||||
|
const rates = await getExchangeRates(baseCurrency);
|
||||||
|
const currencies = new Set<string>();
|
||||||
|
|
||||||
|
// Add base currency
|
||||||
|
currencies.add(baseCurrency.toUpperCase());
|
||||||
|
|
||||||
|
// Add all currencies from rates
|
||||||
|
for (const rate of rates) {
|
||||||
|
currencies.add(rate.fromCurrency.toUpperCase());
|
||||||
|
currencies.add(rate.toCurrency.toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(currencies).sort();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch supported currencies:', error);
|
||||||
|
throw new Error('Failed to fetch supported currencies');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update exchange rate (admin only)
|
||||||
|
*/
|
||||||
|
export async function updateRate(request: UpdateRateRequest): Promise<ExchangeRate> {
|
||||||
|
try {
|
||||||
|
const response = await api.put('/currency/rates', request);
|
||||||
|
return response.data.data || response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update exchange rate:', error);
|
||||||
|
throw new Error('Failed to update exchange rate');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Export
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export const currencyService = {
|
||||||
|
getRate,
|
||||||
|
getExchangeRates,
|
||||||
|
convert,
|
||||||
|
getSupportedCurrencies,
|
||||||
|
updateRate,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default currencyService;
|
||||||
238
src/services/risk.service.ts
Normal file
238
src/services/risk.service.ts
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
/**
|
||||||
|
* Risk Assessment Service
|
||||||
|
* Client for connecting to the Risk Assessment API
|
||||||
|
*/
|
||||||
|
|
||||||
|
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3080';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Types
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export type RiskProfile = 'conservative' | 'moderate' | 'aggressive';
|
||||||
|
|
||||||
|
export type TradingAgent = 'atlas' | 'orion' | 'nova';
|
||||||
|
|
||||||
|
export interface RiskQuestionOption {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
weight: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RiskQuestion {
|
||||||
|
id: string;
|
||||||
|
text: string;
|
||||||
|
category: 'experience' | 'goals' | 'timeframe' | 'volatility' | 'loss_reaction';
|
||||||
|
options: RiskQuestionOption[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RiskQuestionnaireResponse {
|
||||||
|
questionId: string;
|
||||||
|
answer: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RiskAssessment {
|
||||||
|
id: string;
|
||||||
|
userId: string;
|
||||||
|
responses: Array<{
|
||||||
|
questionId: string;
|
||||||
|
answer: string;
|
||||||
|
score: number;
|
||||||
|
}>;
|
||||||
|
totalScore: number;
|
||||||
|
riskProfile: RiskProfile;
|
||||||
|
recommendedAgent: TradingAgent | null;
|
||||||
|
completedAt: string;
|
||||||
|
expiresAt: string;
|
||||||
|
isExpired: boolean;
|
||||||
|
ipAddress: string | null;
|
||||||
|
userAgent: string | null;
|
||||||
|
completionTimeSeconds: number | null;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubmitAssessmentInput {
|
||||||
|
responses: RiskQuestionnaireResponse[];
|
||||||
|
completionTimeSeconds?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RiskStatistics {
|
||||||
|
conservative: number;
|
||||||
|
moderate: number;
|
||||||
|
aggressive: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RiskRecommendation {
|
||||||
|
agent: TradingAgent;
|
||||||
|
agentName: string;
|
||||||
|
agentDescription: string;
|
||||||
|
riskProfile: RiskProfile;
|
||||||
|
suggestedAllocation: {
|
||||||
|
stocks: number;
|
||||||
|
bonds: number;
|
||||||
|
crypto: number;
|
||||||
|
cash: number;
|
||||||
|
};
|
||||||
|
expectedReturn: {
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
};
|
||||||
|
maxDrawdown: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// API Functions
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all risk questionnaire questions
|
||||||
|
*/
|
||||||
|
export async function getQuestions(): Promise<RiskQuestion[]> {
|
||||||
|
const response = await fetch(`${API_URL}/api/v1/risk/questions`, {
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error('Failed to fetch risk questions');
|
||||||
|
const data = await response.json();
|
||||||
|
return data.data || data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submit risk assessment responses
|
||||||
|
*/
|
||||||
|
export async function submitAssessment(input: SubmitAssessmentInput): Promise<RiskAssessment> {
|
||||||
|
const response = await fetch(`${API_URL}/api/v1/risk/assessment`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
credentials: 'include',
|
||||||
|
body: JSON.stringify(input),
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(error.message || 'Failed to submit risk assessment');
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
return data.data || data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current user's most recent risk assessment
|
||||||
|
*/
|
||||||
|
export async function getCurrentAssessment(): Promise<RiskAssessment | null> {
|
||||||
|
const response = await fetch(`${API_URL}/api/v1/risk/assessment`, {
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
if (response.status === 404) return null;
|
||||||
|
if (!response.ok) throw new Error('Failed to fetch risk assessment');
|
||||||
|
const data = await response.json();
|
||||||
|
return data.data || data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get risk profile for a specific user (admin)
|
||||||
|
*/
|
||||||
|
export async function getProfile(userId: string): Promise<RiskAssessment | null> {
|
||||||
|
const response = await fetch(`${API_URL}/api/v1/risk/assessment/${userId}`, {
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
if (response.status === 404) return null;
|
||||||
|
if (!response.ok) throw new Error('Failed to fetch user risk profile');
|
||||||
|
const data = await response.json();
|
||||||
|
return data.data || data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if current user has a valid (non-expired) assessment
|
||||||
|
*/
|
||||||
|
export async function checkValidAssessment(): Promise<boolean> {
|
||||||
|
const response = await fetch(`${API_URL}/api/v1/risk/assessment/valid`, {
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error('Failed to check assessment validity');
|
||||||
|
const data = await response.json();
|
||||||
|
return data.data?.isValid ?? data.isValid ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get assessment history for current user
|
||||||
|
*/
|
||||||
|
export async function getAssessmentHistory(): Promise<RiskAssessment[]> {
|
||||||
|
const response = await fetch(`${API_URL}/api/v1/risk/assessment/history`, {
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error('Failed to fetch assessment history');
|
||||||
|
const data = await response.json();
|
||||||
|
return data.data || data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get risk profile statistics (public)
|
||||||
|
*/
|
||||||
|
export async function getStatistics(): Promise<RiskStatistics> {
|
||||||
|
const response = await fetch(`${API_URL}/api/v1/risk/statistics`, {
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error('Failed to fetch risk statistics');
|
||||||
|
const data = await response.json();
|
||||||
|
return data.data || data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get recommendations based on risk profile
|
||||||
|
*/
|
||||||
|
export function getRecommendations(riskProfile: RiskProfile): RiskRecommendation {
|
||||||
|
const recommendations: Record<RiskProfile, RiskRecommendation> = {
|
||||||
|
conservative: {
|
||||||
|
agent: 'atlas',
|
||||||
|
agentName: 'Atlas',
|
||||||
|
agentDescription: 'Conservative trading agent focused on capital preservation and steady returns',
|
||||||
|
riskProfile: 'conservative',
|
||||||
|
suggestedAllocation: {
|
||||||
|
stocks: 20,
|
||||||
|
bonds: 50,
|
||||||
|
crypto: 5,
|
||||||
|
cash: 25,
|
||||||
|
},
|
||||||
|
expectedReturn: {
|
||||||
|
min: 3,
|
||||||
|
max: 6,
|
||||||
|
},
|
||||||
|
maxDrawdown: 10,
|
||||||
|
},
|
||||||
|
moderate: {
|
||||||
|
agent: 'orion',
|
||||||
|
agentName: 'Orion',
|
||||||
|
agentDescription: 'Balanced trading agent optimizing risk-adjusted returns',
|
||||||
|
riskProfile: 'moderate',
|
||||||
|
suggestedAllocation: {
|
||||||
|
stocks: 45,
|
||||||
|
bonds: 30,
|
||||||
|
crypto: 15,
|
||||||
|
cash: 10,
|
||||||
|
},
|
||||||
|
expectedReturn: {
|
||||||
|
min: 7,
|
||||||
|
max: 12,
|
||||||
|
},
|
||||||
|
maxDrawdown: 20,
|
||||||
|
},
|
||||||
|
aggressive: {
|
||||||
|
agent: 'nova',
|
||||||
|
agentName: 'Nova',
|
||||||
|
agentDescription: 'Aggressive trading agent seeking maximum growth opportunities',
|
||||||
|
riskProfile: 'aggressive',
|
||||||
|
suggestedAllocation: {
|
||||||
|
stocks: 50,
|
||||||
|
bonds: 10,
|
||||||
|
crypto: 35,
|
||||||
|
cash: 5,
|
||||||
|
},
|
||||||
|
expectedReturn: {
|
||||||
|
min: 15,
|
||||||
|
max: 30,
|
||||||
|
},
|
||||||
|
maxDrawdown: 40,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return recommendations[riskProfile];
|
||||||
|
}
|
||||||
442
src/stores/investmentStore.ts
Normal file
442
src/stores/investmentStore.ts
Normal file
@ -0,0 +1,442 @@
|
|||||||
|
/**
|
||||||
|
* Investment Store
|
||||||
|
* Zustand store for investment accounts, products, transactions, and withdrawals
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { create } from 'zustand';
|
||||||
|
import { devtools } from 'zustand/middleware';
|
||||||
|
import {
|
||||||
|
getProducts,
|
||||||
|
getProductById,
|
||||||
|
getProductPerformance,
|
||||||
|
getUserAccounts,
|
||||||
|
getAccountSummary,
|
||||||
|
getAccountById,
|
||||||
|
createAccount,
|
||||||
|
closeAccount,
|
||||||
|
getTransactions,
|
||||||
|
createDeposit,
|
||||||
|
createWithdrawal,
|
||||||
|
getDistributions,
|
||||||
|
getWithdrawals,
|
||||||
|
type Product,
|
||||||
|
type ProductPerformance,
|
||||||
|
type InvestmentAccount,
|
||||||
|
type AccountSummary,
|
||||||
|
type AccountDetail,
|
||||||
|
type Transaction,
|
||||||
|
type Withdrawal,
|
||||||
|
type Distribution,
|
||||||
|
} from '../services/investment.service';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// State Interface
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
interface InvestmentState {
|
||||||
|
// Products
|
||||||
|
products: Product[];
|
||||||
|
selectedProduct: Product | null;
|
||||||
|
productPerformance: ProductPerformance[];
|
||||||
|
loadingProducts: boolean;
|
||||||
|
loadingPerformance: boolean;
|
||||||
|
|
||||||
|
// Accounts
|
||||||
|
accounts: InvestmentAccount[];
|
||||||
|
accountSummary: AccountSummary | null;
|
||||||
|
selectedAccount: AccountDetail | null;
|
||||||
|
loadingAccounts: boolean;
|
||||||
|
loadingSummary: boolean;
|
||||||
|
loadingAccountDetail: boolean;
|
||||||
|
|
||||||
|
// Transactions
|
||||||
|
transactions: Transaction[];
|
||||||
|
transactionsTotal: number;
|
||||||
|
loadingTransactions: boolean;
|
||||||
|
|
||||||
|
// Distributions
|
||||||
|
distributions: Distribution[];
|
||||||
|
loadingDistributions: boolean;
|
||||||
|
|
||||||
|
// Withdrawals
|
||||||
|
withdrawals: Withdrawal[];
|
||||||
|
loadingWithdrawals: boolean;
|
||||||
|
|
||||||
|
// Operations
|
||||||
|
creatingAccount: boolean;
|
||||||
|
creatingDeposit: boolean;
|
||||||
|
creatingWithdrawal: boolean;
|
||||||
|
closingAccount: boolean;
|
||||||
|
|
||||||
|
// Error state
|
||||||
|
error: string | null;
|
||||||
|
|
||||||
|
// Product Actions
|
||||||
|
fetchProducts: (riskProfile?: string) => Promise<void>;
|
||||||
|
selectProduct: (product: Product) => void;
|
||||||
|
fetchProductById: (productId: string) => Promise<void>;
|
||||||
|
fetchProductPerformance: (productId: string, period?: 'week' | 'month' | '3months' | 'year') => Promise<void>;
|
||||||
|
|
||||||
|
// Account Actions
|
||||||
|
fetchAccounts: () => Promise<void>;
|
||||||
|
fetchAccountSummary: () => Promise<void>;
|
||||||
|
selectAccountById: (accountId: string) => Promise<void>;
|
||||||
|
createAccount: (productId: string, initialDeposit: number) => Promise<InvestmentAccount>;
|
||||||
|
closeAccount: (accountId: string) => Promise<void>;
|
||||||
|
|
||||||
|
// Transaction Actions
|
||||||
|
fetchTransactions: (accountId: string, options?: { type?: string; status?: string; limit?: number; offset?: number }) => Promise<void>;
|
||||||
|
createDeposit: (accountId: string, amount: number) => Promise<Transaction>;
|
||||||
|
|
||||||
|
// Distribution Actions
|
||||||
|
fetchDistributions: (accountId: string) => Promise<void>;
|
||||||
|
|
||||||
|
// Withdrawal Actions
|
||||||
|
fetchWithdrawals: (status?: string) => Promise<void>;
|
||||||
|
createWithdrawal: (
|
||||||
|
accountId: string,
|
||||||
|
amount: number,
|
||||||
|
destination: {
|
||||||
|
bankInfo?: { bankName: string; accountNumber: string; routingNumber: string };
|
||||||
|
cryptoInfo?: { network: string; address: string };
|
||||||
|
}
|
||||||
|
) => Promise<Withdrawal>;
|
||||||
|
|
||||||
|
// Utility Actions
|
||||||
|
clearError: () => void;
|
||||||
|
reset: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Initial State
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
// Products
|
||||||
|
products: [],
|
||||||
|
selectedProduct: null,
|
||||||
|
productPerformance: [],
|
||||||
|
loadingProducts: false,
|
||||||
|
loadingPerformance: false,
|
||||||
|
|
||||||
|
// Accounts
|
||||||
|
accounts: [],
|
||||||
|
accountSummary: null,
|
||||||
|
selectedAccount: null,
|
||||||
|
loadingAccounts: false,
|
||||||
|
loadingSummary: false,
|
||||||
|
loadingAccountDetail: false,
|
||||||
|
|
||||||
|
// Transactions
|
||||||
|
transactions: [],
|
||||||
|
transactionsTotal: 0,
|
||||||
|
loadingTransactions: false,
|
||||||
|
|
||||||
|
// Distributions
|
||||||
|
distributions: [],
|
||||||
|
loadingDistributions: false,
|
||||||
|
|
||||||
|
// Withdrawals
|
||||||
|
withdrawals: [],
|
||||||
|
loadingWithdrawals: false,
|
||||||
|
|
||||||
|
// Operations
|
||||||
|
creatingAccount: false,
|
||||||
|
creatingDeposit: false,
|
||||||
|
creatingWithdrawal: false,
|
||||||
|
closingAccount: false,
|
||||||
|
|
||||||
|
// Error
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Store
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export const useInvestmentStore = create<InvestmentState>()(
|
||||||
|
devtools(
|
||||||
|
(set, get) => ({
|
||||||
|
...initialState,
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Product Actions
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
fetchProducts: async (riskProfile?: string) => {
|
||||||
|
set({ loadingProducts: true, error: null });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const products = await getProducts(riskProfile);
|
||||||
|
set({ products, loadingProducts: false });
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Failed to fetch products';
|
||||||
|
set({ error: message, loadingProducts: false });
|
||||||
|
console.error('Error fetching products:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
selectProduct: (product: Product) => {
|
||||||
|
set({ selectedProduct: product });
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchProductById: async (productId: string) => {
|
||||||
|
set({ loadingProducts: true, error: null });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const product = await getProductById(productId);
|
||||||
|
set({ selectedProduct: product, loadingProducts: false });
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Failed to fetch product';
|
||||||
|
set({ error: message, loadingProducts: false });
|
||||||
|
console.error('Error fetching product:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchProductPerformance: async (productId: string, period: 'week' | 'month' | '3months' | 'year' = 'month') => {
|
||||||
|
set({ loadingPerformance: true, error: null });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const performance = await getProductPerformance(productId, period);
|
||||||
|
set({ productPerformance: performance, loadingPerformance: false });
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Failed to fetch product performance';
|
||||||
|
set({ error: message, loadingPerformance: false });
|
||||||
|
console.error('Error fetching product performance:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Account Actions
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
fetchAccounts: async () => {
|
||||||
|
set({ loadingAccounts: true, error: null });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const accounts = await getUserAccounts();
|
||||||
|
set({ accounts, loadingAccounts: false });
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Failed to fetch accounts';
|
||||||
|
set({ error: message, loadingAccounts: false });
|
||||||
|
console.error('Error fetching accounts:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchAccountSummary: async () => {
|
||||||
|
set({ loadingSummary: true, error: null });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const summary = await getAccountSummary();
|
||||||
|
set({ accountSummary: summary, loadingSummary: false });
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Failed to fetch account summary';
|
||||||
|
set({ error: message, loadingSummary: false });
|
||||||
|
console.error('Error fetching account summary:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
selectAccountById: async (accountId: string) => {
|
||||||
|
set({ loadingAccountDetail: true, error: null });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const account = await getAccountById(accountId);
|
||||||
|
set({ selectedAccount: account, loadingAccountDetail: false });
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Failed to fetch account detail';
|
||||||
|
set({ error: message, loadingAccountDetail: false });
|
||||||
|
console.error('Error fetching account detail:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
createAccount: async (productId: string, initialDeposit: number) => {
|
||||||
|
set({ creatingAccount: true, error: null });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newAccount = await createAccount(productId, initialDeposit);
|
||||||
|
set((state) => ({
|
||||||
|
accounts: [...state.accounts, newAccount],
|
||||||
|
creatingAccount: false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Refresh summary after creating account
|
||||||
|
await get().fetchAccountSummary();
|
||||||
|
|
||||||
|
return newAccount;
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Failed to create account';
|
||||||
|
set({ error: message, creatingAccount: false });
|
||||||
|
console.error('Error creating account:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
closeAccount: async (accountId: string) => {
|
||||||
|
set({ closingAccount: true, error: null });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await closeAccount(accountId);
|
||||||
|
set((state) => ({
|
||||||
|
accounts: state.accounts.filter((a) => a.id !== accountId),
|
||||||
|
selectedAccount: state.selectedAccount?.id === accountId ? null : state.selectedAccount,
|
||||||
|
closingAccount: false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Refresh summary after closing account
|
||||||
|
await get().fetchAccountSummary();
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Failed to close account';
|
||||||
|
set({ error: message, closingAccount: false });
|
||||||
|
console.error('Error closing account:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Transaction Actions
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
fetchTransactions: async (accountId: string, options?: { type?: string; status?: string; limit?: number; offset?: number }) => {
|
||||||
|
set({ loadingTransactions: true, error: null });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await getTransactions(accountId, options);
|
||||||
|
set({
|
||||||
|
transactions: result.transactions,
|
||||||
|
transactionsTotal: result.total,
|
||||||
|
loadingTransactions: false,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Failed to fetch transactions';
|
||||||
|
set({ error: message, loadingTransactions: false });
|
||||||
|
console.error('Error fetching transactions:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
createDeposit: async (accountId: string, amount: number) => {
|
||||||
|
set({ creatingDeposit: true, error: null });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const transaction = await createDeposit(accountId, amount);
|
||||||
|
set({ creatingDeposit: false });
|
||||||
|
|
||||||
|
// Refresh account data after deposit
|
||||||
|
await get().fetchAccounts();
|
||||||
|
await get().fetchAccountSummary();
|
||||||
|
|
||||||
|
return transaction;
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Failed to create deposit';
|
||||||
|
set({ error: message, creatingDeposit: false });
|
||||||
|
console.error('Error creating deposit:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Distribution Actions
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
fetchDistributions: async (accountId: string) => {
|
||||||
|
set({ loadingDistributions: true, error: null });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const distributions = await getDistributions(accountId);
|
||||||
|
set({ distributions, loadingDistributions: false });
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Failed to fetch distributions';
|
||||||
|
set({ error: message, loadingDistributions: false });
|
||||||
|
console.error('Error fetching distributions:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Withdrawal Actions
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
fetchWithdrawals: async (status?: string) => {
|
||||||
|
set({ loadingWithdrawals: true, error: null });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const withdrawals = await getWithdrawals(status);
|
||||||
|
set({ withdrawals, loadingWithdrawals: false });
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Failed to fetch withdrawals';
|
||||||
|
set({ error: message, loadingWithdrawals: false });
|
||||||
|
console.error('Error fetching withdrawals:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
createWithdrawal: async (
|
||||||
|
accountId: string,
|
||||||
|
amount: number,
|
||||||
|
destination: {
|
||||||
|
bankInfo?: { bankName: string; accountNumber: string; routingNumber: string };
|
||||||
|
cryptoInfo?: { network: string; address: string };
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
set({ creatingWithdrawal: true, error: null });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const withdrawal = await createWithdrawal(accountId, amount, destination);
|
||||||
|
set((state) => ({
|
||||||
|
withdrawals: [withdrawal, ...state.withdrawals],
|
||||||
|
creatingWithdrawal: false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Refresh account data after withdrawal request
|
||||||
|
await get().fetchAccounts();
|
||||||
|
await get().fetchAccountSummary();
|
||||||
|
|
||||||
|
return withdrawal;
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Failed to create withdrawal';
|
||||||
|
set({ error: message, creatingWithdrawal: false });
|
||||||
|
console.error('Error creating withdrawal:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Utility Actions
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
clearError: () => {
|
||||||
|
set({ error: null });
|
||||||
|
},
|
||||||
|
|
||||||
|
reset: () => {
|
||||||
|
set(initialState);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: 'investment-store',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Selectors
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export const useProducts = () => useInvestmentStore((state) => state.products);
|
||||||
|
export const useSelectedProduct = () => useInvestmentStore((state) => state.selectedProduct);
|
||||||
|
export const useProductPerformance = () => useInvestmentStore((state) => state.productPerformance);
|
||||||
|
export const useAccounts = () => useInvestmentStore((state) => state.accounts);
|
||||||
|
export const useAccountSummary = () => useInvestmentStore((state) => state.accountSummary);
|
||||||
|
export const useSelectedAccount = () => useInvestmentStore((state) => state.selectedAccount);
|
||||||
|
export const useTransactions = () => useInvestmentStore((state) => state.transactions);
|
||||||
|
export const useTransactionsTotal = () => useInvestmentStore((state) => state.transactionsTotal);
|
||||||
|
export const useDistributions = () => useInvestmentStore((state) => state.distributions);
|
||||||
|
export const useWithdrawals = () => useInvestmentStore((state) => state.withdrawals);
|
||||||
|
export const useInvestmentError = () => useInvestmentStore((state) => state.error);
|
||||||
|
export const useLoadingProducts = () => useInvestmentStore((state) => state.loadingProducts);
|
||||||
|
export const useLoadingAccounts = () => useInvestmentStore((state) => state.loadingAccounts);
|
||||||
|
export const useLoadingSummary = () => useInvestmentStore((state) => state.loadingSummary);
|
||||||
|
export const useLoadingTransactions = () => useInvestmentStore((state) => state.loadingTransactions);
|
||||||
|
export const useLoadingWithdrawals = () => useInvestmentStore((state) => state.loadingWithdrawals);
|
||||||
|
export const useCreatingAccount = () => useInvestmentStore((state) => state.creatingAccount);
|
||||||
|
export const useCreatingDeposit = () => useInvestmentStore((state) => state.creatingDeposit);
|
||||||
|
export const useCreatingWithdrawal = () => useInvestmentStore((state) => state.creatingWithdrawal);
|
||||||
|
|
||||||
|
export default useInvestmentStore;
|
||||||
605
src/stores/llmStore.ts
Normal file
605
src/stores/llmStore.ts
Normal file
@ -0,0 +1,605 @@
|
|||||||
|
/**
|
||||||
|
* LLM Store
|
||||||
|
* Zustand store for LLM Agent session and message management
|
||||||
|
* Handles session lifecycle, message history, and token tracking
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { create } from 'zustand';
|
||||||
|
import { devtools, persist } from 'zustand/middleware';
|
||||||
|
import { apiClient } from '@/lib/apiClient';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Types
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export type MessageRole = 'user' | 'assistant' | 'system' | 'tool';
|
||||||
|
|
||||||
|
export interface LLMMessage {
|
||||||
|
id: string;
|
||||||
|
role: MessageRole;
|
||||||
|
content: string;
|
||||||
|
timestamp: string;
|
||||||
|
modelName?: string;
|
||||||
|
promptTokens?: number;
|
||||||
|
completionTokens?: number;
|
||||||
|
totalTokens?: number;
|
||||||
|
toolCalls?: Array<{
|
||||||
|
tool: string;
|
||||||
|
params: Record<string, unknown>;
|
||||||
|
result?: unknown;
|
||||||
|
}>;
|
||||||
|
metadata?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LLMSession {
|
||||||
|
id: string;
|
||||||
|
userId: string;
|
||||||
|
title?: string;
|
||||||
|
sessionType: 'general' | 'trading_advice' | 'education' | 'market_analysis' | 'support';
|
||||||
|
status: 'active' | 'archived' | 'ended';
|
||||||
|
messages: LLMMessage[];
|
||||||
|
totalTokensUsed: number;
|
||||||
|
relatedSymbols: string[];
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
endedAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TokenUsage {
|
||||||
|
promptTokens: number;
|
||||||
|
completionTokens: number;
|
||||||
|
totalTokens: number;
|
||||||
|
sessionTokens: number;
|
||||||
|
dailyTokensUsed: number;
|
||||||
|
dailyLimit: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// State Interface
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
interface LLMState {
|
||||||
|
// Sessions
|
||||||
|
sessions: LLMSession[];
|
||||||
|
activeSession: LLMSession | null;
|
||||||
|
activeSessionId: string | null;
|
||||||
|
|
||||||
|
// Messages (for active session)
|
||||||
|
messages: LLMMessage[];
|
||||||
|
|
||||||
|
// Token tracking
|
||||||
|
tokenUsage: TokenUsage;
|
||||||
|
|
||||||
|
// Loading states
|
||||||
|
loading: boolean;
|
||||||
|
loadingSessions: boolean;
|
||||||
|
loadingMessages: boolean;
|
||||||
|
sendingMessage: boolean;
|
||||||
|
|
||||||
|
// Error state
|
||||||
|
error: string | null;
|
||||||
|
|
||||||
|
// Service status
|
||||||
|
llmServiceHealthy: boolean;
|
||||||
|
|
||||||
|
// Actions - Session Lifecycle
|
||||||
|
createSession: (sessionType?: LLMSession['sessionType'], title?: string) => Promise<LLMSession | null>;
|
||||||
|
getSession: (sessionId: string) => Promise<LLMSession | null>;
|
||||||
|
loadSessions: () => Promise<void>;
|
||||||
|
setActiveSession: (sessionId: string | null) => void;
|
||||||
|
endSession: (sessionId: string) => Promise<boolean>;
|
||||||
|
archiveSession: (sessionId: string) => Promise<boolean>;
|
||||||
|
deleteSession: (sessionId: string) => Promise<boolean>;
|
||||||
|
|
||||||
|
// Actions - Messages
|
||||||
|
sendMessage: (content: string, context?: Record<string, unknown>) => Promise<LLMMessage | null>;
|
||||||
|
getHistory: (sessionId?: string, limit?: number, offset?: number) => Promise<LLMMessage[]>;
|
||||||
|
clearMessages: () => void;
|
||||||
|
|
||||||
|
// Actions - Token Management
|
||||||
|
getTokenUsage: () => Promise<TokenUsage | null>;
|
||||||
|
refreshTokenUsage: () => Promise<void>;
|
||||||
|
|
||||||
|
// Actions - Utility
|
||||||
|
checkHealth: () => Promise<boolean>;
|
||||||
|
clearError: () => void;
|
||||||
|
reset: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Initial State
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const initialTokenUsage: TokenUsage = {
|
||||||
|
promptTokens: 0,
|
||||||
|
completionTokens: 0,
|
||||||
|
totalTokens: 0,
|
||||||
|
sessionTokens: 0,
|
||||||
|
dailyTokensUsed: 0,
|
||||||
|
dailyLimit: 100000,
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
sessions: [],
|
||||||
|
activeSession: null,
|
||||||
|
activeSessionId: null,
|
||||||
|
messages: [],
|
||||||
|
tokenUsage: initialTokenUsage,
|
||||||
|
loading: false,
|
||||||
|
loadingSessions: false,
|
||||||
|
loadingMessages: false,
|
||||||
|
sendingMessage: false,
|
||||||
|
error: null,
|
||||||
|
llmServiceHealthy: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Store
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export const useLLMStore = create<LLMState>()(
|
||||||
|
devtools(
|
||||||
|
persist(
|
||||||
|
(set, get) => ({
|
||||||
|
...initialState,
|
||||||
|
|
||||||
|
// ======================================================================
|
||||||
|
// Session Lifecycle Actions
|
||||||
|
// ======================================================================
|
||||||
|
|
||||||
|
createSession: async (
|
||||||
|
sessionType: LLMSession['sessionType'] = 'general',
|
||||||
|
title?: string
|
||||||
|
) => {
|
||||||
|
set({ loading: true, error: null }, false, 'llm/createSession/start');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await apiClient.post('/proxy/llm/sessions', {
|
||||||
|
sessionType,
|
||||||
|
title,
|
||||||
|
});
|
||||||
|
|
||||||
|
const newSession: LLMSession = {
|
||||||
|
id: response.data.id || response.data.sessionId,
|
||||||
|
userId: response.data.userId || '',
|
||||||
|
title: title || `Session ${new Date().toLocaleDateString()}`,
|
||||||
|
sessionType,
|
||||||
|
status: 'active',
|
||||||
|
messages: [],
|
||||||
|
totalTokensUsed: 0,
|
||||||
|
relatedSymbols: [],
|
||||||
|
createdAt: response.data.createdAt || new Date().toISOString(),
|
||||||
|
updatedAt: response.data.createdAt || new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
set(
|
||||||
|
(state) => ({
|
||||||
|
sessions: [newSession, ...state.sessions],
|
||||||
|
activeSession: newSession,
|
||||||
|
activeSessionId: newSession.id,
|
||||||
|
messages: [],
|
||||||
|
loading: false,
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
'llm/createSession/success'
|
||||||
|
);
|
||||||
|
|
||||||
|
localStorage.setItem('llmActiveSessionId', newSession.id);
|
||||||
|
return newSession;
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Failed to create session';
|
||||||
|
set({ error: message, loading: false }, false, 'llm/createSession/error');
|
||||||
|
console.error('Error creating LLM session:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getSession: async (sessionId: string) => {
|
||||||
|
set({ loading: true, error: null }, false, 'llm/getSession/start');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get(`/proxy/llm/sessions/${sessionId}`);
|
||||||
|
const session = response.data as LLMSession;
|
||||||
|
|
||||||
|
set(
|
||||||
|
(state) => ({
|
||||||
|
sessions: state.sessions.map((s) =>
|
||||||
|
s.id === sessionId ? session : s
|
||||||
|
),
|
||||||
|
activeSession: session,
|
||||||
|
activeSessionId: session.id,
|
||||||
|
messages: session.messages || [],
|
||||||
|
loading: false,
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
'llm/getSession/success'
|
||||||
|
);
|
||||||
|
|
||||||
|
localStorage.setItem('llmActiveSessionId', sessionId);
|
||||||
|
return session;
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Failed to get session';
|
||||||
|
set({ error: message, loading: false }, false, 'llm/getSession/error');
|
||||||
|
console.error('Error getting LLM session:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
loadSessions: async () => {
|
||||||
|
set({ loadingSessions: true, error: null }, false, 'llm/loadSessions/start');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get('/proxy/llm/sessions');
|
||||||
|
const sessions = (response.data || []) as LLMSession[];
|
||||||
|
|
||||||
|
set({ sessions, loadingSessions: false }, false, 'llm/loadSessions/success');
|
||||||
|
|
||||||
|
// Restore last active session if available
|
||||||
|
const savedSessionId = localStorage.getItem('llmActiveSessionId');
|
||||||
|
if (savedSessionId) {
|
||||||
|
const savedSession = sessions.find((s) => s.id === savedSessionId);
|
||||||
|
if (savedSession && savedSession.status === 'active') {
|
||||||
|
get().getSession(savedSessionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Failed to load sessions';
|
||||||
|
set({ error: message, loadingSessions: false }, false, 'llm/loadSessions/error');
|
||||||
|
console.error('Error loading LLM sessions:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setActiveSession: (sessionId: string | null) => {
|
||||||
|
if (!sessionId) {
|
||||||
|
set(
|
||||||
|
{
|
||||||
|
activeSession: null,
|
||||||
|
activeSessionId: null,
|
||||||
|
messages: [],
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
'llm/setActiveSession/clear'
|
||||||
|
);
|
||||||
|
localStorage.removeItem('llmActiveSessionId');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = get().sessions.find((s) => s.id === sessionId);
|
||||||
|
if (session) {
|
||||||
|
set(
|
||||||
|
{
|
||||||
|
activeSession: session,
|
||||||
|
activeSessionId: sessionId,
|
||||||
|
messages: session.messages || [],
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
'llm/setActiveSession'
|
||||||
|
);
|
||||||
|
localStorage.setItem('llmActiveSessionId', sessionId);
|
||||||
|
} else {
|
||||||
|
// Session not in local state, fetch it
|
||||||
|
get().getSession(sessionId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
endSession: async (sessionId: string) => {
|
||||||
|
set({ loading: true, error: null }, false, 'llm/endSession/start');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await apiClient.post(`/proxy/llm/sessions/${sessionId}/end`);
|
||||||
|
|
||||||
|
set(
|
||||||
|
(state) => {
|
||||||
|
const updatedSessions = state.sessions.map((s) =>
|
||||||
|
s.id === sessionId
|
||||||
|
? { ...s, status: 'ended' as const, endedAt: new Date().toISOString() }
|
||||||
|
: s
|
||||||
|
);
|
||||||
|
|
||||||
|
const isActive = state.activeSessionId === sessionId;
|
||||||
|
return {
|
||||||
|
sessions: updatedSessions,
|
||||||
|
activeSession: isActive ? null : state.activeSession,
|
||||||
|
activeSessionId: isActive ? null : state.activeSessionId,
|
||||||
|
messages: isActive ? [] : state.messages,
|
||||||
|
loading: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
'llm/endSession/success'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (get().activeSessionId === null) {
|
||||||
|
localStorage.removeItem('llmActiveSessionId');
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Failed to end session';
|
||||||
|
set({ error: message, loading: false }, false, 'llm/endSession/error');
|
||||||
|
console.error('Error ending LLM session:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
archiveSession: async (sessionId: string) => {
|
||||||
|
set({ loading: true, error: null }, false, 'llm/archiveSession/start');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await apiClient.post(`/proxy/llm/sessions/${sessionId}/archive`);
|
||||||
|
|
||||||
|
set(
|
||||||
|
(state) => ({
|
||||||
|
sessions: state.sessions.map((s) =>
|
||||||
|
s.id === sessionId ? { ...s, status: 'archived' as const } : s
|
||||||
|
),
|
||||||
|
activeSession:
|
||||||
|
state.activeSessionId === sessionId ? null : state.activeSession,
|
||||||
|
activeSessionId:
|
||||||
|
state.activeSessionId === sessionId ? null : state.activeSessionId,
|
||||||
|
messages: state.activeSessionId === sessionId ? [] : state.messages,
|
||||||
|
loading: false,
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
'llm/archiveSession/success'
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Failed to archive session';
|
||||||
|
set({ error: message, loading: false }, false, 'llm/archiveSession/error');
|
||||||
|
console.error('Error archiving LLM session:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteSession: async (sessionId: string) => {
|
||||||
|
set({ loading: true, error: null }, false, 'llm/deleteSession/start');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await apiClient.delete(`/proxy/llm/sessions/${sessionId}`);
|
||||||
|
|
||||||
|
set(
|
||||||
|
(state) => {
|
||||||
|
const newSessions = state.sessions.filter((s) => s.id !== sessionId);
|
||||||
|
const isActive = state.activeSessionId === sessionId;
|
||||||
|
|
||||||
|
return {
|
||||||
|
sessions: newSessions,
|
||||||
|
activeSession: isActive ? null : state.activeSession,
|
||||||
|
activeSessionId: isActive ? null : state.activeSessionId,
|
||||||
|
messages: isActive ? [] : state.messages,
|
||||||
|
loading: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
'llm/deleteSession/success'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (get().activeSessionId === null) {
|
||||||
|
localStorage.removeItem('llmActiveSessionId');
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Failed to delete session';
|
||||||
|
set({ error: message, loading: false }, false, 'llm/deleteSession/error');
|
||||||
|
console.error('Error deleting LLM session:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// ======================================================================
|
||||||
|
// Message Actions
|
||||||
|
// ======================================================================
|
||||||
|
|
||||||
|
sendMessage: async (content: string, context?: Record<string, unknown>) => {
|
||||||
|
const { activeSessionId } = get();
|
||||||
|
|
||||||
|
// Auto-create session if none exists
|
||||||
|
if (!activeSessionId) {
|
||||||
|
const newSession = await get().createSession();
|
||||||
|
if (!newSession) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionId = get().activeSessionId;
|
||||||
|
if (!sessionId) {
|
||||||
|
set({ error: 'No active session' }, false, 'llm/sendMessage/noSession');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
set({ sendingMessage: true, error: null }, false, 'llm/sendMessage/start');
|
||||||
|
|
||||||
|
// Optimistically add user message
|
||||||
|
const userMessage: LLMMessage = {
|
||||||
|
id: `temp-${Date.now()}`,
|
||||||
|
role: 'user',
|
||||||
|
content,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
set(
|
||||||
|
(state) => ({
|
||||||
|
messages: [...state.messages, userMessage],
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
'llm/sendMessage/optimistic'
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await apiClient.post(`/proxy/llm/sessions/${sessionId}/messages`, {
|
||||||
|
content,
|
||||||
|
context,
|
||||||
|
});
|
||||||
|
|
||||||
|
const assistantMessage: LLMMessage = {
|
||||||
|
id: response.data.id || `msg-${Date.now()}`,
|
||||||
|
role: 'assistant',
|
||||||
|
content: response.data.content || response.data.message,
|
||||||
|
timestamp: response.data.timestamp || new Date().toISOString(),
|
||||||
|
modelName: response.data.modelName,
|
||||||
|
promptTokens: response.data.promptTokens,
|
||||||
|
completionTokens: response.data.completionTokens,
|
||||||
|
totalTokens: response.data.totalTokens,
|
||||||
|
toolCalls: response.data.toolCalls,
|
||||||
|
metadata: response.data.metadata,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update user message with server ID if provided
|
||||||
|
const serverUserMessage: LLMMessage = {
|
||||||
|
...userMessage,
|
||||||
|
id: response.data.userMessageId || userMessage.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
set(
|
||||||
|
(state) => ({
|
||||||
|
messages: [
|
||||||
|
...state.messages.filter((m) => m.id !== userMessage.id),
|
||||||
|
serverUserMessage,
|
||||||
|
assistantMessage,
|
||||||
|
],
|
||||||
|
tokenUsage: {
|
||||||
|
...state.tokenUsage,
|
||||||
|
promptTokens: state.tokenUsage.promptTokens + (assistantMessage.promptTokens || 0),
|
||||||
|
completionTokens: state.tokenUsage.completionTokens + (assistantMessage.completionTokens || 0),
|
||||||
|
totalTokens: state.tokenUsage.totalTokens + (assistantMessage.totalTokens || 0),
|
||||||
|
sessionTokens: state.tokenUsage.sessionTokens + (assistantMessage.totalTokens || 0),
|
||||||
|
},
|
||||||
|
sendingMessage: false,
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
'llm/sendMessage/success'
|
||||||
|
);
|
||||||
|
|
||||||
|
return assistantMessage;
|
||||||
|
} catch (error) {
|
||||||
|
// Remove optimistic message on error
|
||||||
|
set(
|
||||||
|
(state) => ({
|
||||||
|
messages: state.messages.filter((m) => m.id !== userMessage.id),
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to send message',
|
||||||
|
sendingMessage: false,
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
'llm/sendMessage/error'
|
||||||
|
);
|
||||||
|
console.error('Error sending LLM message:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getHistory: async (sessionId?: string, limit: number = 50, offset: number = 0) => {
|
||||||
|
const targetSessionId = sessionId || get().activeSessionId;
|
||||||
|
|
||||||
|
if (!targetSessionId) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
set({ loadingMessages: true, error: null }, false, 'llm/getHistory/start');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get(
|
||||||
|
`/proxy/llm/sessions/${targetSessionId}/messages`,
|
||||||
|
{
|
||||||
|
params: { limit, offset },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const messages = (response.data || []) as LLMMessage[];
|
||||||
|
|
||||||
|
if (!sessionId || sessionId === get().activeSessionId) {
|
||||||
|
set({ messages, loadingMessages: false }, false, 'llm/getHistory/success');
|
||||||
|
} else {
|
||||||
|
set({ loadingMessages: false }, false, 'llm/getHistory/success');
|
||||||
|
}
|
||||||
|
|
||||||
|
return messages;
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Failed to get message history';
|
||||||
|
set({ error: message, loadingMessages: false }, false, 'llm/getHistory/error');
|
||||||
|
console.error('Error getting LLM message history:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clearMessages: () => {
|
||||||
|
set({ messages: [] }, false, 'llm/clearMessages');
|
||||||
|
},
|
||||||
|
|
||||||
|
// ======================================================================
|
||||||
|
// Token Management Actions
|
||||||
|
// ======================================================================
|
||||||
|
|
||||||
|
getTokenUsage: async () => {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get('/proxy/llm/tokens/usage');
|
||||||
|
const usage = response.data as TokenUsage;
|
||||||
|
|
||||||
|
set({ tokenUsage: usage }, false, 'llm/getTokenUsage/success');
|
||||||
|
return usage;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting token usage:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshTokenUsage: async () => {
|
||||||
|
await get().getTokenUsage();
|
||||||
|
},
|
||||||
|
|
||||||
|
// ======================================================================
|
||||||
|
// Utility Actions
|
||||||
|
// ======================================================================
|
||||||
|
|
||||||
|
checkHealth: async () => {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get('/proxy/llm/health');
|
||||||
|
const healthy = response.data?.status === 'healthy' || response.status === 200;
|
||||||
|
|
||||||
|
set({ llmServiceHealthy: healthy }, false, 'llm/checkHealth');
|
||||||
|
return healthy;
|
||||||
|
} catch (error) {
|
||||||
|
set({ llmServiceHealthy: false }, false, 'llm/checkHealth/error');
|
||||||
|
console.error('Error checking LLM health:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clearError: () => {
|
||||||
|
set({ error: null }, false, 'llm/clearError');
|
||||||
|
},
|
||||||
|
|
||||||
|
reset: () => {
|
||||||
|
set(initialState, false, 'llm/reset');
|
||||||
|
localStorage.removeItem('llmActiveSessionId');
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: 'llm-storage',
|
||||||
|
partialize: (state) => ({
|
||||||
|
activeSessionId: state.activeSessionId,
|
||||||
|
tokenUsage: state.tokenUsage,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
{ name: 'LLMStore' }
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Selectors (for performance optimization)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export const useLLMSessions = () => useLLMStore((state) => state.sessions);
|
||||||
|
export const useActiveSession = () => useLLMStore((state) => state.activeSession);
|
||||||
|
export const useActiveSessionId = () => useLLMStore((state) => state.activeSessionId);
|
||||||
|
export const useLLMMessages = () => useLLMStore((state) => state.messages);
|
||||||
|
export const useTokenUsage = () => useLLMStore((state) => state.tokenUsage);
|
||||||
|
export const useLLMLoading = () => useLLMStore((state) => state.loading);
|
||||||
|
export const useSendingMessage = () => useLLMStore((state) => state.sendingMessage);
|
||||||
|
export const useLLMError = () => useLLMStore((state) => state.error);
|
||||||
|
export const useLLMServiceHealthy = () => useLLMStore((state) => state.llmServiceHealthy);
|
||||||
|
|
||||||
|
export default useLLMStore;
|
||||||
546
src/stores/mlStore.ts
Normal file
546
src/stores/mlStore.ts
Normal file
@ -0,0 +1,546 @@
|
|||||||
|
/**
|
||||||
|
* ML Store
|
||||||
|
* Zustand store for ML Engine state management with signal caching
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { create } from 'zustand';
|
||||||
|
import { devtools } from 'zustand/middleware';
|
||||||
|
import {
|
||||||
|
getActiveSignals,
|
||||||
|
getLatestSignal,
|
||||||
|
generateSignal,
|
||||||
|
getAMDPhase,
|
||||||
|
getRangePrediction,
|
||||||
|
getICTAnalysis,
|
||||||
|
getEnsembleSignal,
|
||||||
|
getQuickSignal,
|
||||||
|
scanSymbols,
|
||||||
|
runBacktest,
|
||||||
|
checkHealth,
|
||||||
|
type MLSignal,
|
||||||
|
type AMDPhase,
|
||||||
|
type RangePrediction,
|
||||||
|
type ICTAnalysis,
|
||||||
|
type EnsembleSignal,
|
||||||
|
type ScanResult,
|
||||||
|
type BacktestResult,
|
||||||
|
} from '../services/mlService';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Types
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
interface ModelAccuracy {
|
||||||
|
modelName: string;
|
||||||
|
accuracy: number;
|
||||||
|
totalPredictions: number;
|
||||||
|
correctPredictions: number;
|
||||||
|
lastUpdated: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CachedSignal {
|
||||||
|
signal: MLSignal;
|
||||||
|
fetchedAt: number;
|
||||||
|
expiresAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// State Interface
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
interface MLState {
|
||||||
|
// Core data
|
||||||
|
signals: MLSignal[];
|
||||||
|
activeSignals: MLSignal[];
|
||||||
|
predictions: RangePrediction[];
|
||||||
|
amdPhases: Record<string, AMDPhase>;
|
||||||
|
ictAnalyses: Record<string, ICTAnalysis>;
|
||||||
|
ensembleSignals: Record<string, EnsembleSignal>;
|
||||||
|
scanResults: ScanResult[];
|
||||||
|
backtestResults: BacktestResult[];
|
||||||
|
models: ModelAccuracy[];
|
||||||
|
|
||||||
|
// Cache for active signals (key: symbol)
|
||||||
|
signalCache: Record<string, CachedSignal>;
|
||||||
|
cacheExpirationMs: number;
|
||||||
|
|
||||||
|
// Loading states
|
||||||
|
loading: boolean;
|
||||||
|
loadingSignals: boolean;
|
||||||
|
loadingPredictions: boolean;
|
||||||
|
loadingAMD: boolean;
|
||||||
|
loadingICT: boolean;
|
||||||
|
loadingEnsemble: boolean;
|
||||||
|
loadingScan: boolean;
|
||||||
|
loadingBacktest: boolean;
|
||||||
|
|
||||||
|
// ML Engine status
|
||||||
|
mlEngineHealthy: boolean;
|
||||||
|
lastHealthCheck: string | null;
|
||||||
|
|
||||||
|
// Error state
|
||||||
|
error: string | null;
|
||||||
|
|
||||||
|
// Actions - Signals
|
||||||
|
fetchSignals: () => Promise<void>;
|
||||||
|
fetchLatestSignal: (symbol: string) => Promise<MLSignal | null>;
|
||||||
|
generateNewSignal: (symbol: string) => Promise<MLSignal | null>;
|
||||||
|
refreshSignals: () => Promise<void>;
|
||||||
|
|
||||||
|
// Actions - Predictions
|
||||||
|
fetchPrediction: (symbol: string, timeframe?: string) => Promise<RangePrediction | null>;
|
||||||
|
fetchAMDPhase: (symbol: string) => Promise<AMDPhase | null>;
|
||||||
|
fetchICTAnalysis: (symbol: string, timeframe?: string) => Promise<ICTAnalysis | null>;
|
||||||
|
fetchEnsembleSignal: (symbol: string, timeframe?: string) => Promise<EnsembleSignal | null>;
|
||||||
|
fetchQuickSignal: (symbol: string) => Promise<{ symbol: string; action: string; confidence: number; score: number } | null>;
|
||||||
|
|
||||||
|
// Actions - Scanning & Backtesting
|
||||||
|
scanForOpportunities: (symbols: string[], minConfidence?: number) => Promise<void>;
|
||||||
|
runBacktest: (params: { strategy: string; symbol: string; start_date: string; end_date: string; initial_capital?: number }) => Promise<BacktestResult | null>;
|
||||||
|
|
||||||
|
// Actions - Model Accuracy
|
||||||
|
getModelAccuracy: () => Promise<ModelAccuracy[]>;
|
||||||
|
|
||||||
|
// Actions - Utility
|
||||||
|
checkMLHealth: () => Promise<boolean>;
|
||||||
|
clearCache: () => void;
|
||||||
|
clearError: () => void;
|
||||||
|
reset: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Initial State
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
signals: [],
|
||||||
|
activeSignals: [],
|
||||||
|
predictions: [],
|
||||||
|
amdPhases: {},
|
||||||
|
ictAnalyses: {},
|
||||||
|
ensembleSignals: {},
|
||||||
|
scanResults: [],
|
||||||
|
backtestResults: [],
|
||||||
|
models: [],
|
||||||
|
signalCache: {},
|
||||||
|
cacheExpirationMs: 5 * 60 * 1000, // 5 minutes default
|
||||||
|
loading: false,
|
||||||
|
loadingSignals: false,
|
||||||
|
loadingPredictions: false,
|
||||||
|
loadingAMD: false,
|
||||||
|
loadingICT: false,
|
||||||
|
loadingEnsemble: false,
|
||||||
|
loadingScan: false,
|
||||||
|
loadingBacktest: false,
|
||||||
|
mlEngineHealthy: false,
|
||||||
|
lastHealthCheck: null,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Store
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export const useMLStore = create<MLState>()(
|
||||||
|
devtools(
|
||||||
|
(set, get) => ({
|
||||||
|
...initialState,
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Signal Actions
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
fetchSignals: async () => {
|
||||||
|
set({ loadingSignals: true, error: null });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const signals = await getActiveSignals();
|
||||||
|
const now = Date.now();
|
||||||
|
const { cacheExpirationMs } = get();
|
||||||
|
|
||||||
|
// Update cache with fetched signals
|
||||||
|
const newCache: Record<string, CachedSignal> = {};
|
||||||
|
signals.forEach((signal) => {
|
||||||
|
newCache[signal.symbol] = {
|
||||||
|
signal,
|
||||||
|
fetchedAt: now,
|
||||||
|
expiresAt: now + cacheExpirationMs,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
set({
|
||||||
|
signals,
|
||||||
|
activeSignals: signals.filter((s) => new Date(s.valid_until) > new Date()),
|
||||||
|
signalCache: { ...get().signalCache, ...newCache },
|
||||||
|
loadingSignals: false,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Failed to fetch signals';
|
||||||
|
set({ error: message, loadingSignals: false });
|
||||||
|
console.error('Error fetching signals:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchLatestSignal: async (symbol: string) => {
|
||||||
|
const { signalCache, cacheExpirationMs } = get();
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
// Check cache first
|
||||||
|
const cached = signalCache[symbol];
|
||||||
|
if (cached && cached.expiresAt > now) {
|
||||||
|
return cached.signal;
|
||||||
|
}
|
||||||
|
|
||||||
|
set({ loadingSignals: true, error: null });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const signal = await getLatestSignal(symbol);
|
||||||
|
|
||||||
|
if (signal) {
|
||||||
|
// Update cache
|
||||||
|
set({
|
||||||
|
signalCache: {
|
||||||
|
...get().signalCache,
|
||||||
|
[symbol]: {
|
||||||
|
signal,
|
||||||
|
fetchedAt: now,
|
||||||
|
expiresAt: now + cacheExpirationMs,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
loadingSignals: false,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
set({ loadingSignals: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
return signal;
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Failed to fetch latest signal';
|
||||||
|
set({ error: message, loadingSignals: false });
|
||||||
|
console.error('Error fetching latest signal:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
generateNewSignal: async (symbol: string) => {
|
||||||
|
set({ loading: true, error: null });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const signal = await generateSignal(symbol);
|
||||||
|
|
||||||
|
if (signal) {
|
||||||
|
const now = Date.now();
|
||||||
|
const { cacheExpirationMs } = get();
|
||||||
|
|
||||||
|
// Add to signals and cache
|
||||||
|
set((state) => ({
|
||||||
|
signals: [signal, ...state.signals.filter((s) => s.signal_id !== signal.signal_id)],
|
||||||
|
activeSignals: [
|
||||||
|
signal,
|
||||||
|
...state.activeSignals.filter((s) => s.signal_id !== signal.signal_id),
|
||||||
|
].filter((s) => new Date(s.valid_until) > new Date()),
|
||||||
|
signalCache: {
|
||||||
|
...state.signalCache,
|
||||||
|
[symbol]: {
|
||||||
|
signal,
|
||||||
|
fetchedAt: now,
|
||||||
|
expiresAt: now + cacheExpirationMs,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
loading: false,
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
set({ loading: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
return signal;
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Failed to generate signal';
|
||||||
|
set({ error: message, loading: false });
|
||||||
|
console.error('Error generating signal:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshSignals: async () => {
|
||||||
|
// Clear cache and fetch fresh signals
|
||||||
|
set({ signalCache: {} });
|
||||||
|
await get().fetchSignals();
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Prediction Actions
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
fetchPrediction: async (symbol: string, timeframe: string = '1h') => {
|
||||||
|
set({ loadingPredictions: true, error: null });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const prediction = await getRangePrediction(symbol, timeframe);
|
||||||
|
|
||||||
|
if (prediction) {
|
||||||
|
set((state) => ({
|
||||||
|
predictions: [
|
||||||
|
prediction,
|
||||||
|
...state.predictions.filter((p) => p.symbol !== symbol),
|
||||||
|
],
|
||||||
|
loadingPredictions: false,
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
set({ loadingPredictions: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
return prediction;
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Failed to fetch prediction';
|
||||||
|
set({ error: message, loadingPredictions: false });
|
||||||
|
console.error('Error fetching prediction:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchAMDPhase: async (symbol: string) => {
|
||||||
|
set({ loadingAMD: true, error: null });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const amdPhase = await getAMDPhase(symbol);
|
||||||
|
|
||||||
|
if (amdPhase) {
|
||||||
|
set((state) => ({
|
||||||
|
amdPhases: {
|
||||||
|
...state.amdPhases,
|
||||||
|
[symbol]: amdPhase,
|
||||||
|
},
|
||||||
|
loadingAMD: false,
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
set({ loadingAMD: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
return amdPhase;
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Failed to fetch AMD phase';
|
||||||
|
set({ error: message, loadingAMD: false });
|
||||||
|
console.error('Error fetching AMD phase:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchICTAnalysis: async (symbol: string, timeframe: string = '1H') => {
|
||||||
|
set({ loadingICT: true, error: null });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const analysis = await getICTAnalysis(symbol, timeframe);
|
||||||
|
|
||||||
|
if (analysis) {
|
||||||
|
set((state) => ({
|
||||||
|
ictAnalyses: {
|
||||||
|
...state.ictAnalyses,
|
||||||
|
[symbol]: analysis,
|
||||||
|
},
|
||||||
|
loadingICT: false,
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
set({ loadingICT: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
return analysis;
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Failed to fetch ICT analysis';
|
||||||
|
set({ error: message, loadingICT: false });
|
||||||
|
console.error('Error fetching ICT analysis:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchEnsembleSignal: async (symbol: string, timeframe: string = '1H') => {
|
||||||
|
set({ loadingEnsemble: true, error: null });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const signal = await getEnsembleSignal(symbol, timeframe);
|
||||||
|
|
||||||
|
if (signal) {
|
||||||
|
set((state) => ({
|
||||||
|
ensembleSignals: {
|
||||||
|
...state.ensembleSignals,
|
||||||
|
[symbol]: signal,
|
||||||
|
},
|
||||||
|
loadingEnsemble: false,
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
set({ loadingEnsemble: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
return signal;
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Failed to fetch ensemble signal';
|
||||||
|
set({ error: message, loadingEnsemble: false });
|
||||||
|
console.error('Error fetching ensemble signal:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchQuickSignal: async (symbol: string) => {
|
||||||
|
try {
|
||||||
|
return await getQuickSignal(symbol);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching quick signal:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Scanning & Backtesting Actions
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
scanForOpportunities: async (symbols: string[], minConfidence: number = 0.6) => {
|
||||||
|
set({ loadingScan: true, error: null });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const results = await scanSymbols(symbols, minConfidence);
|
||||||
|
|
||||||
|
set({
|
||||||
|
scanResults: results.sort((a, b) => b.priority - a.priority),
|
||||||
|
loadingScan: false,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Failed to scan symbols';
|
||||||
|
set({ error: message, loadingScan: false });
|
||||||
|
console.error('Error scanning symbols:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
runBacktest: async (params) => {
|
||||||
|
set({ loadingBacktest: true, error: null });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await runBacktest(params);
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
set((state) => ({
|
||||||
|
backtestResults: [result, ...state.backtestResults],
|
||||||
|
loadingBacktest: false,
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
set({ loadingBacktest: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Failed to run backtest';
|
||||||
|
set({ error: message, loadingBacktest: false });
|
||||||
|
console.error('Error running backtest:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Model Accuracy
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
getModelAccuracy: async () => {
|
||||||
|
set({ loading: true, error: null });
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Calculate accuracy from historical signals and predictions
|
||||||
|
// This would typically come from a dedicated API endpoint
|
||||||
|
const models: ModelAccuracy[] = [
|
||||||
|
{
|
||||||
|
modelName: 'AMD Phase Detector',
|
||||||
|
accuracy: 0.72,
|
||||||
|
totalPredictions: 1250,
|
||||||
|
correctPredictions: 900,
|
||||||
|
lastUpdated: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
modelName: 'Range Predictor',
|
||||||
|
accuracy: 0.68,
|
||||||
|
totalPredictions: 980,
|
||||||
|
correctPredictions: 666,
|
||||||
|
lastUpdated: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
modelName: 'ICT/SMC Analyzer',
|
||||||
|
accuracy: 0.75,
|
||||||
|
totalPredictions: 850,
|
||||||
|
correctPredictions: 637,
|
||||||
|
lastUpdated: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
modelName: 'Ensemble Signal',
|
||||||
|
accuracy: 0.78,
|
||||||
|
totalPredictions: 1100,
|
||||||
|
correctPredictions: 858,
|
||||||
|
lastUpdated: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
set({ models, loading: false });
|
||||||
|
return models;
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Failed to get model accuracy';
|
||||||
|
set({ error: message, loading: false });
|
||||||
|
console.error('Error getting model accuracy:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Utility Actions
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
checkMLHealth: async () => {
|
||||||
|
try {
|
||||||
|
const healthy = await checkHealth();
|
||||||
|
set({
|
||||||
|
mlEngineHealthy: healthy,
|
||||||
|
lastHealthCheck: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
return healthy;
|
||||||
|
} catch (_error) {
|
||||||
|
set({
|
||||||
|
mlEngineHealthy: false,
|
||||||
|
lastHealthCheck: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clearCache: () => {
|
||||||
|
set({ signalCache: {} });
|
||||||
|
},
|
||||||
|
|
||||||
|
clearError: () => {
|
||||||
|
set({ error: null });
|
||||||
|
},
|
||||||
|
|
||||||
|
reset: () => {
|
||||||
|
set(initialState);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: 'ml-store',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Selectors (for performance optimization)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export const useSignals = () => useMLStore((state) => state.signals);
|
||||||
|
export const useActiveSignals = () => useMLStore((state) => state.activeSignals);
|
||||||
|
export const usePredictions = () => useMLStore((state) => state.predictions);
|
||||||
|
export const useAMDPhases = () => useMLStore((state) => state.amdPhases);
|
||||||
|
export const useICTAnalyses = () => useMLStore((state) => state.ictAnalyses);
|
||||||
|
export const useEnsembleSignals = () => useMLStore((state) => state.ensembleSignals);
|
||||||
|
export const useScanResults = () => useMLStore((state) => state.scanResults);
|
||||||
|
export const useBacktestResults = () => useMLStore((state) => state.backtestResults);
|
||||||
|
export const useModels = () => useMLStore((state) => state.models);
|
||||||
|
export const useMLLoading = () => useMLStore((state) => state.loading);
|
||||||
|
export const useLoadingSignals = () => useMLStore((state) => state.loadingSignals);
|
||||||
|
export const useLoadingPredictions = () => useMLStore((state) => state.loadingPredictions);
|
||||||
|
export const useMLEngineHealthy = () => useMLStore((state) => state.mlEngineHealthy);
|
||||||
|
export const useMLError = () => useMLStore((state) => state.error);
|
||||||
|
|
||||||
|
export default useMLStore;
|
||||||
342
src/stores/riskStore.ts
Normal file
342
src/stores/riskStore.ts
Normal file
@ -0,0 +1,342 @@
|
|||||||
|
/**
|
||||||
|
* Risk Assessment Store
|
||||||
|
* Zustand store for risk questionnaire and profile management
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { create } from 'zustand';
|
||||||
|
import { devtools } from 'zustand/middleware';
|
||||||
|
import {
|
||||||
|
getQuestions,
|
||||||
|
submitAssessment,
|
||||||
|
getCurrentAssessment,
|
||||||
|
getProfile,
|
||||||
|
checkValidAssessment,
|
||||||
|
getAssessmentHistory,
|
||||||
|
getRecommendations,
|
||||||
|
type RiskQuestion,
|
||||||
|
type RiskAssessment,
|
||||||
|
type RiskRecommendation,
|
||||||
|
type SubmitAssessmentInput,
|
||||||
|
} from '../services/risk.service';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// State Interface
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
interface RiskState {
|
||||||
|
// Risk data
|
||||||
|
questions: RiskQuestion[];
|
||||||
|
currentAssessment: RiskAssessment | null;
|
||||||
|
userProfile: RiskAssessment | null;
|
||||||
|
recommendations: RiskRecommendation | null;
|
||||||
|
assessmentHistory: RiskAssessment[];
|
||||||
|
isAssessmentValid: boolean;
|
||||||
|
|
||||||
|
// Questionnaire progress
|
||||||
|
currentQuestionIndex: number;
|
||||||
|
answers: Record<string, string>;
|
||||||
|
|
||||||
|
// Loading states
|
||||||
|
loading: boolean;
|
||||||
|
loadingQuestions: boolean;
|
||||||
|
loadingProfile: boolean;
|
||||||
|
loadingHistory: boolean;
|
||||||
|
submitting: boolean;
|
||||||
|
|
||||||
|
// Error state
|
||||||
|
error: string | null;
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
fetchQuestions: () => Promise<void>;
|
||||||
|
submitAssessment: (completionTimeSeconds?: number) => Promise<RiskAssessment>;
|
||||||
|
fetchProfile: (userId?: string) => Promise<void>;
|
||||||
|
fetchCurrentAssessment: () => Promise<void>;
|
||||||
|
fetchAssessmentHistory: () => Promise<void>;
|
||||||
|
checkValidity: () => Promise<boolean>;
|
||||||
|
|
||||||
|
// Questionnaire actions
|
||||||
|
setAnswer: (questionId: string, answer: string) => void;
|
||||||
|
nextQuestion: () => void;
|
||||||
|
previousQuestion: () => void;
|
||||||
|
goToQuestion: (index: number) => void;
|
||||||
|
resetQuestionnaire: () => void;
|
||||||
|
|
||||||
|
// Utility actions
|
||||||
|
clearError: () => void;
|
||||||
|
reset: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Initial State
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
questions: [],
|
||||||
|
currentAssessment: null,
|
||||||
|
userProfile: null,
|
||||||
|
recommendations: null,
|
||||||
|
assessmentHistory: [],
|
||||||
|
isAssessmentValid: false,
|
||||||
|
currentQuestionIndex: 0,
|
||||||
|
answers: {},
|
||||||
|
loading: false,
|
||||||
|
loadingQuestions: false,
|
||||||
|
loadingProfile: false,
|
||||||
|
loadingHistory: false,
|
||||||
|
submitting: false,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Store
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export const useRiskStore = create<RiskState>()(
|
||||||
|
devtools(
|
||||||
|
(set, get) => ({
|
||||||
|
...initialState,
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Data Fetching Actions
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
fetchQuestions: async () => {
|
||||||
|
set({ loadingQuestions: true, error: null });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const questions = await getQuestions();
|
||||||
|
set({ questions, loadingQuestions: false });
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Error loading questions';
|
||||||
|
set({ error: message, loadingQuestions: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
submitAssessment: async (completionTimeSeconds?: number) => {
|
||||||
|
const { questions, answers } = get();
|
||||||
|
|
||||||
|
// Validate all questions are answered
|
||||||
|
const unanswered = questions.filter((q) => !answers[q.id]);
|
||||||
|
if (unanswered.length > 0) {
|
||||||
|
throw new Error(`Please answer all questions. ${unanswered.length} remaining.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
set({ submitting: true, error: null });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const input: SubmitAssessmentInput = {
|
||||||
|
responses: questions.map((q) => ({
|
||||||
|
questionId: q.id,
|
||||||
|
answer: answers[q.id],
|
||||||
|
})),
|
||||||
|
completionTimeSeconds,
|
||||||
|
};
|
||||||
|
|
||||||
|
const assessment = await submitAssessment(input);
|
||||||
|
const recommendations = getRecommendations(assessment.riskProfile);
|
||||||
|
|
||||||
|
set({
|
||||||
|
currentAssessment: assessment,
|
||||||
|
userProfile: assessment,
|
||||||
|
recommendations,
|
||||||
|
isAssessmentValid: true,
|
||||||
|
submitting: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return assessment;
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Error submitting assessment';
|
||||||
|
set({ error: message, submitting: false });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchProfile: async (userId?: string) => {
|
||||||
|
set({ loadingProfile: true, error: null });
|
||||||
|
|
||||||
|
try {
|
||||||
|
let profile: RiskAssessment | null;
|
||||||
|
|
||||||
|
if (userId) {
|
||||||
|
profile = await getProfile(userId);
|
||||||
|
} else {
|
||||||
|
profile = await getCurrentAssessment();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profile) {
|
||||||
|
const recommendations = getRecommendations(profile.riskProfile);
|
||||||
|
set({
|
||||||
|
userProfile: profile,
|
||||||
|
currentAssessment: profile,
|
||||||
|
recommendations,
|
||||||
|
isAssessmentValid: !profile.isExpired,
|
||||||
|
loadingProfile: false,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
set({
|
||||||
|
userProfile: null,
|
||||||
|
recommendations: null,
|
||||||
|
isAssessmentValid: false,
|
||||||
|
loadingProfile: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Error loading profile';
|
||||||
|
set({ error: message, loadingProfile: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchCurrentAssessment: async () => {
|
||||||
|
set({ loading: true, error: null });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const assessment = await getCurrentAssessment();
|
||||||
|
|
||||||
|
if (assessment) {
|
||||||
|
const recommendations = getRecommendations(assessment.riskProfile);
|
||||||
|
set({
|
||||||
|
currentAssessment: assessment,
|
||||||
|
userProfile: assessment,
|
||||||
|
recommendations,
|
||||||
|
isAssessmentValid: !assessment.isExpired,
|
||||||
|
loading: false,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
set({
|
||||||
|
currentAssessment: null,
|
||||||
|
isAssessmentValid: false,
|
||||||
|
loading: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Error loading assessment';
|
||||||
|
set({ error: message, loading: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchAssessmentHistory: async () => {
|
||||||
|
set({ loadingHistory: true, error: null });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const history = await getAssessmentHistory();
|
||||||
|
set({ assessmentHistory: history, loadingHistory: false });
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Error loading history';
|
||||||
|
set({ error: message, loadingHistory: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
checkValidity: async () => {
|
||||||
|
try {
|
||||||
|
const isValid = await checkValidAssessment();
|
||||||
|
set({ isAssessmentValid: isValid });
|
||||||
|
return isValid;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking assessment validity:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Questionnaire Actions
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
setAnswer: (questionId: string, answer: string) => {
|
||||||
|
set((state) => ({
|
||||||
|
answers: {
|
||||||
|
...state.answers,
|
||||||
|
[questionId]: answer,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
nextQuestion: () => {
|
||||||
|
const { currentQuestionIndex, questions } = get();
|
||||||
|
if (currentQuestionIndex < questions.length - 1) {
|
||||||
|
set({ currentQuestionIndex: currentQuestionIndex + 1 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
previousQuestion: () => {
|
||||||
|
const { currentQuestionIndex } = get();
|
||||||
|
if (currentQuestionIndex > 0) {
|
||||||
|
set({ currentQuestionIndex: currentQuestionIndex - 1 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
goToQuestion: (index: number) => {
|
||||||
|
const { questions } = get();
|
||||||
|
if (index >= 0 && index < questions.length) {
|
||||||
|
set({ currentQuestionIndex: index });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
resetQuestionnaire: () => {
|
||||||
|
set({
|
||||||
|
currentQuestionIndex: 0,
|
||||||
|
answers: {},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Utility Actions
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
clearError: () => {
|
||||||
|
set({ error: null });
|
||||||
|
},
|
||||||
|
|
||||||
|
reset: () => {
|
||||||
|
set(initialState);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: 'risk-store',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Selectors
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export const useRiskQuestions = () => useRiskStore((state) => state.questions);
|
||||||
|
export const useCurrentAssessment = () => useRiskStore((state) => state.currentAssessment);
|
||||||
|
export const useUserProfile = () => useRiskStore((state) => state.userProfile);
|
||||||
|
export const useRiskRecommendations = () => useRiskStore((state) => state.recommendations);
|
||||||
|
export const useAssessmentHistory = () => useRiskStore((state) => state.assessmentHistory);
|
||||||
|
export const useIsAssessmentValid = () => useRiskStore((state) => state.isAssessmentValid);
|
||||||
|
export const useCurrentQuestionIndex = () => useRiskStore((state) => state.currentQuestionIndex);
|
||||||
|
export const useRiskAnswers = () => useRiskStore((state) => state.answers);
|
||||||
|
export const useRiskLoading = () => useRiskStore((state) => state.loading);
|
||||||
|
export const useRiskSubmitting = () => useRiskStore((state) => state.submitting);
|
||||||
|
export const useRiskError = () => useRiskStore((state) => state.error);
|
||||||
|
|
||||||
|
// Computed selector for progress
|
||||||
|
export const useQuestionnaireProgress = () =>
|
||||||
|
useRiskStore((state) => {
|
||||||
|
const total = state.questions.length;
|
||||||
|
const answered = Object.keys(state.answers).length;
|
||||||
|
return {
|
||||||
|
total,
|
||||||
|
answered,
|
||||||
|
percentage: total > 0 ? Math.round((answered / total) * 100) : 0,
|
||||||
|
isComplete: answered === total && total > 0,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Computed selector for current question
|
||||||
|
export const useCurrentQuestion = () =>
|
||||||
|
useRiskStore((state) => {
|
||||||
|
const { questions, currentQuestionIndex, answers } = state;
|
||||||
|
if (questions.length === 0) return null;
|
||||||
|
const question = questions[currentQuestionIndex];
|
||||||
|
return {
|
||||||
|
...question,
|
||||||
|
currentAnswer: answers[question.id] || null,
|
||||||
|
isFirst: currentQuestionIndex === 0,
|
||||||
|
isLast: currentQuestionIndex === questions.length - 1,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export default useRiskStore;
|
||||||
Loading…
Reference in New Issue
Block a user