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