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:
Adrian Flores Cortes 2026-01-30 12:24:49 -06:00
parent 5779c9a5cf
commit 6d0673a799
7 changed files with 2707 additions and 0 deletions

View 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;

View 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;

View 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];
}

View 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
View 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
View 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
View 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;