[OQI-003] feat: Add AlertsPanel component for price alerts management
- Create AlertsPanel component with full CRUD functionality - Add alerts API functions to trading.service.ts - Integrate AlertsPanel into Trading.tsx with toggle button - Support for above/below/crosses conditions - Push and email notification options - Recurring alerts support - Filter by active/inactive status Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
639c70587a
commit
49d6492c91
457
src/modules/trading/components/AlertsPanel.tsx
Normal file
457
src/modules/trading/components/AlertsPanel.tsx
Normal file
@ -0,0 +1,457 @@
|
||||
/**
|
||||
* AlertsPanel Component
|
||||
* Manages price alerts for trading symbols
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import {
|
||||
BellIcon,
|
||||
BellAlertIcon,
|
||||
PlusIcon,
|
||||
TrashIcon,
|
||||
ArrowPathIcon,
|
||||
CheckCircleIcon,
|
||||
XCircleIcon,
|
||||
ArrowUpIcon,
|
||||
ArrowDownIcon,
|
||||
} from '@heroicons/react/24/solid';
|
||||
import {
|
||||
getAlerts,
|
||||
createAlert,
|
||||
deleteAlert,
|
||||
enableAlert,
|
||||
disableAlert,
|
||||
getAlertStats,
|
||||
type PriceAlert,
|
||||
type AlertCondition,
|
||||
type AlertStats,
|
||||
type CreateAlertInput,
|
||||
} from '../../../services/trading.service';
|
||||
|
||||
interface AlertsPanelProps {
|
||||
symbol: string;
|
||||
currentPrice?: number;
|
||||
}
|
||||
|
||||
export const AlertsPanel: React.FC<AlertsPanelProps> = ({ symbol, currentPrice }) => {
|
||||
const [alerts, setAlerts] = useState<PriceAlert[]>([]);
|
||||
const [stats, setStats] = useState<AlertStats | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [filterActive, setFilterActive] = useState<boolean | undefined>(undefined);
|
||||
|
||||
// Form state
|
||||
const [formPrice, setFormPrice] = useState<string>('');
|
||||
const [formCondition, setFormCondition] = useState<AlertCondition>('above');
|
||||
const [formNote, setFormNote] = useState('');
|
||||
const [formNotifyPush, setFormNotifyPush] = useState(true);
|
||||
const [formNotifyEmail, setFormNotifyEmail] = useState(false);
|
||||
const [formIsRecurring, setFormIsRecurring] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
// Fetch alerts
|
||||
const fetchAlerts = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const [alertsData, statsData] = await Promise.all([
|
||||
getAlerts({ isActive: filterActive }),
|
||||
getAlertStats(),
|
||||
]);
|
||||
setAlerts(alertsData);
|
||||
setStats(statsData);
|
||||
} catch (err) {
|
||||
setError('Failed to fetch alerts');
|
||||
console.error('Alerts fetch error:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [filterActive]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchAlerts();
|
||||
}, [fetchAlerts]);
|
||||
|
||||
// Set current price when showing form
|
||||
useEffect(() => {
|
||||
if (showForm && currentPrice) {
|
||||
setFormPrice(currentPrice.toFixed(2));
|
||||
}
|
||||
}, [showForm, currentPrice]);
|
||||
|
||||
// Handle create alert
|
||||
const handleCreateAlert = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!formPrice || isNaN(parseFloat(formPrice))) return;
|
||||
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const input: CreateAlertInput = {
|
||||
symbol: symbol.toUpperCase(),
|
||||
condition: formCondition,
|
||||
price: parseFloat(formPrice),
|
||||
note: formNote || undefined,
|
||||
notifyPush: formNotifyPush,
|
||||
notifyEmail: formNotifyEmail,
|
||||
isRecurring: formIsRecurring,
|
||||
};
|
||||
await createAlert(input);
|
||||
setShowForm(false);
|
||||
setFormPrice('');
|
||||
setFormNote('');
|
||||
setFormCondition('above');
|
||||
setFormIsRecurring(false);
|
||||
fetchAlerts();
|
||||
} catch (err) {
|
||||
setError('Failed to create alert');
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle toggle alert
|
||||
const handleToggleAlert = async (alert: PriceAlert) => {
|
||||
try {
|
||||
if (alert.isActive) {
|
||||
await disableAlert(alert.id);
|
||||
} else {
|
||||
await enableAlert(alert.id);
|
||||
}
|
||||
fetchAlerts();
|
||||
} catch (err) {
|
||||
setError('Failed to toggle alert');
|
||||
}
|
||||
};
|
||||
|
||||
// Handle delete alert
|
||||
const handleDeleteAlert = async (alertId: string) => {
|
||||
try {
|
||||
await deleteAlert(alertId);
|
||||
fetchAlerts();
|
||||
} catch (err) {
|
||||
setError('Failed to delete alert');
|
||||
}
|
||||
};
|
||||
|
||||
// Get condition display
|
||||
const getConditionDisplay = (condition: AlertCondition) => {
|
||||
switch (condition) {
|
||||
case 'above':
|
||||
return { text: 'Price Above', icon: ArrowUpIcon, color: 'text-green-400' };
|
||||
case 'below':
|
||||
return { text: 'Price Below', icon: ArrowDownIcon, color: 'text-red-400' };
|
||||
case 'crosses_above':
|
||||
return { text: 'Crosses Above', icon: ArrowUpIcon, color: 'text-green-400' };
|
||||
case 'crosses_below':
|
||||
return { text: 'Crosses Below', icon: ArrowDownIcon, color: 'text-red-400' };
|
||||
}
|
||||
};
|
||||
|
||||
// Filter alerts for current symbol
|
||||
const symbolAlerts = alerts.filter((a) => a.symbol === symbol.toUpperCase());
|
||||
const otherAlerts = alerts.filter((a) => a.symbol !== symbol.toUpperCase());
|
||||
|
||||
return (
|
||||
<div className="card p-4 space-y-4 h-full overflow-y-auto">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<BellIcon className="w-5 h-5 text-yellow-400" />
|
||||
<h3 className="text-lg font-semibold text-white">Price Alerts</h3>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={fetchAlerts}
|
||||
disabled={loading}
|
||||
className="p-1.5 text-gray-400 hover:text-white hover:bg-gray-700 rounded transition-colors disabled:opacity-50"
|
||||
title="Refresh alerts"
|
||||
>
|
||||
<ArrowPathIcon className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowForm(!showForm)}
|
||||
className="flex items-center gap-1 px-2 py-1 text-sm bg-yellow-600 hover:bg-yellow-700 text-white rounded transition-colors"
|
||||
>
|
||||
<PlusIcon className="w-4 h-4" />
|
||||
<span>New</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
{stats && (
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<div className="text-center p-2 bg-gray-800 rounded">
|
||||
<p className="text-xs text-gray-400">Total</p>
|
||||
<p className="text-lg font-bold text-white">{stats.total}</p>
|
||||
</div>
|
||||
<div className="text-center p-2 bg-green-900/30 rounded">
|
||||
<p className="text-xs text-green-400">Active</p>
|
||||
<p className="text-lg font-bold text-green-400">{stats.active}</p>
|
||||
</div>
|
||||
<div className="text-center p-2 bg-yellow-900/30 rounded">
|
||||
<p className="text-xs text-yellow-400">Triggered</p>
|
||||
<p className="text-lg font-bold text-yellow-400">{stats.triggered}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error message */}
|
||||
{error && (
|
||||
<div className="flex items-center gap-2 p-3 bg-red-900/20 border border-red-800 rounded-lg">
|
||||
<XCircleIcon className="w-5 h-5 text-red-400" />
|
||||
<span className="text-sm text-red-400">{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Create Alert Form */}
|
||||
{showForm && (
|
||||
<form onSubmit={handleCreateAlert} className="space-y-3 p-3 bg-gray-800 rounded-lg border border-gray-700">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-sm font-medium text-white">Create Alert for {symbol}</h4>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowForm(false)}
|
||||
className="text-gray-400 hover:text-white"
|
||||
>
|
||||
<XCircleIcon className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label className="block text-xs text-gray-400 mb-1">Condition</label>
|
||||
<select
|
||||
value={formCondition}
|
||||
onChange={(e) => setFormCondition(e.target.value as AlertCondition)}
|
||||
className="w-full px-2 py-1.5 bg-gray-700 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-yellow-500"
|
||||
>
|
||||
<option value="above">Price Above</option>
|
||||
<option value="below">Price Below</option>
|
||||
<option value="crosses_above">Crosses Above</option>
|
||||
<option value="crosses_below">Crosses Below</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-gray-400 mb-1">Target Price</label>
|
||||
<input
|
||||
type="number"
|
||||
step="any"
|
||||
value={formPrice}
|
||||
onChange={(e) => setFormPrice(e.target.value)}
|
||||
placeholder="0.00"
|
||||
className="w-full px-2 py-1.5 bg-gray-700 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-yellow-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs text-gray-400 mb-1">Note (optional)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formNote}
|
||||
onChange={(e) => setFormNote(e.target.value)}
|
||||
placeholder="Reminder note..."
|
||||
className="w-full px-2 py-1.5 bg-gray-700 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-yellow-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-4 text-sm">
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formNotifyPush}
|
||||
onChange={(e) => setFormNotifyPush(e.target.checked)}
|
||||
className="w-4 h-4 rounded bg-gray-700 border-gray-600 text-yellow-500 focus:ring-yellow-500"
|
||||
/>
|
||||
<span className="text-gray-300">Push notification</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formNotifyEmail}
|
||||
onChange={(e) => setFormNotifyEmail(e.target.checked)}
|
||||
className="w-4 h-4 rounded bg-gray-700 border-gray-600 text-yellow-500 focus:ring-yellow-500"
|
||||
/>
|
||||
<span className="text-gray-300">Email</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formIsRecurring}
|
||||
onChange={(e) => setFormIsRecurring(e.target.checked)}
|
||||
className="w-4 h-4 rounded bg-gray-700 border-gray-600 text-yellow-500 focus:ring-yellow-500"
|
||||
/>
|
||||
<span className="text-gray-300">Recurring</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={submitting || !formPrice}
|
||||
className="w-full py-2 bg-yellow-600 hover:bg-yellow-700 disabled:bg-gray-600 text-white rounded transition-colors font-medium"
|
||||
>
|
||||
{submitting ? 'Creating...' : 'Create Alert'}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{/* Filter Tabs */}
|
||||
<div className="flex gap-1 border-b border-gray-700 pb-2">
|
||||
<button
|
||||
onClick={() => setFilterActive(undefined)}
|
||||
className={`px-3 py-1 text-sm rounded ${
|
||||
filterActive === undefined ? 'bg-gray-700 text-white' : 'text-gray-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
All
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setFilterActive(true)}
|
||||
className={`px-3 py-1 text-sm rounded ${
|
||||
filterActive === true ? 'bg-green-700 text-white' : 'text-gray-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
Active
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setFilterActive(false)}
|
||||
className={`px-3 py-1 text-sm rounded ${
|
||||
filterActive === false ? 'bg-gray-700 text-white' : 'text-gray-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
Inactive
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Alerts for Current Symbol */}
|
||||
{symbolAlerts.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xs font-medium text-gray-400 uppercase">{symbol} Alerts</h4>
|
||||
{symbolAlerts.map((alert) => {
|
||||
const condition = getConditionDisplay(alert.condition);
|
||||
const ConditionIcon = condition.icon;
|
||||
return (
|
||||
<div
|
||||
key={alert.id}
|
||||
className={`p-3 rounded-lg border ${
|
||||
alert.isActive
|
||||
? 'bg-gray-800 border-gray-700'
|
||||
: 'bg-gray-800/50 border-gray-700/50 opacity-60'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<ConditionIcon className={`w-4 h-4 ${condition.color}`} />
|
||||
<span className="text-sm text-gray-300">{condition.text}</span>
|
||||
<span className="font-mono font-bold text-white">${alert.price.toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={() => handleToggleAlert(alert)}
|
||||
className={`p-1 rounded ${
|
||||
alert.isActive
|
||||
? 'text-green-400 hover:bg-green-900/30'
|
||||
: 'text-gray-500 hover:bg-gray-700'
|
||||
}`}
|
||||
title={alert.isActive ? 'Disable' : 'Enable'}
|
||||
>
|
||||
{alert.isActive ? (
|
||||
<CheckCircleIcon className="w-4 h-4" />
|
||||
) : (
|
||||
<XCircleIcon className="w-4 h-4" />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteAlert(alert.id)}
|
||||
className="p-1 text-red-400 hover:bg-red-900/30 rounded"
|
||||
title="Delete"
|
||||
>
|
||||
<TrashIcon className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{alert.note && (
|
||||
<p className="text-xs text-gray-400 mb-1">{alert.note}</p>
|
||||
)}
|
||||
<div className="flex items-center gap-2 text-xs text-gray-500">
|
||||
{alert.isRecurring && (
|
||||
<span className="px-1.5 py-0.5 bg-blue-900/30 text-blue-400 rounded">Recurring</span>
|
||||
)}
|
||||
{alert.triggeredAt && (
|
||||
<span className="flex items-center gap-1">
|
||||
<BellAlertIcon className="w-3 h-3 text-yellow-400" />
|
||||
Triggered at ${alert.triggeredPrice?.toFixed(2)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Other Alerts */}
|
||||
{otherAlerts.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xs font-medium text-gray-400 uppercase">Other Symbols</h4>
|
||||
{otherAlerts.map((alert) => {
|
||||
const condition = getConditionDisplay(alert.condition);
|
||||
const ConditionIcon = condition.icon;
|
||||
return (
|
||||
<div
|
||||
key={alert.id}
|
||||
className={`p-2 rounded-lg border ${
|
||||
alert.isActive
|
||||
? 'bg-gray-800/70 border-gray-700'
|
||||
: 'bg-gray-800/30 border-gray-700/50 opacity-50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-medium text-gray-300">{alert.symbol}</span>
|
||||
<ConditionIcon className={`w-3 h-3 ${condition.color}`} />
|
||||
<span className="font-mono text-sm text-white">${alert.price.toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={() => handleToggleAlert(alert)}
|
||||
className={`p-1 rounded ${
|
||||
alert.isActive ? 'text-green-400' : 'text-gray-500'
|
||||
}`}
|
||||
>
|
||||
{alert.isActive ? (
|
||||
<CheckCircleIcon className="w-3 h-3" />
|
||||
) : (
|
||||
<XCircleIcon className="w-3 h-3" />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteAlert(alert.id)}
|
||||
className="p-1 text-red-400 rounded"
|
||||
>
|
||||
<TrashIcon className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Empty State */}
|
||||
{alerts.length === 0 && !loading && (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<BellIcon className="w-10 h-10 mx-auto mb-3 opacity-50" />
|
||||
<p className="text-sm">No price alerts yet</p>
|
||||
<p className="text-xs mt-1">Create an alert to get notified when price reaches your target</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AlertsPanel;
|
||||
@ -5,6 +5,7 @@ import CandlestickChartWithML from '../components/CandlestickChartWithML';
|
||||
import WatchlistSidebar from '../components/WatchlistSidebar';
|
||||
import PaperTradingPanel from '../components/PaperTradingPanel';
|
||||
import MLSignalsPanel from '../components/MLSignalsPanel';
|
||||
import AlertsPanel from '../components/AlertsPanel';
|
||||
import type { Interval, CrosshairData } from '../../../types/trading.types';
|
||||
import type { MLSignal } from '../../../services/mlService';
|
||||
|
||||
@ -14,6 +15,7 @@ export default function Trading() {
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
|
||||
const [isMLSignalsOpen, setIsMLSignalsOpen] = useState(true);
|
||||
const [isPaperTradingOpen, setIsPaperTradingOpen] = useState(true);
|
||||
const [isAlertsOpen, setIsAlertsOpen] = useState(false);
|
||||
|
||||
// ML Overlay states
|
||||
const [enableMLOverlays, setEnableMLOverlays] = useState(true);
|
||||
@ -239,6 +241,25 @@ export default function Trading() {
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setIsAlertsOpen(!isAlertsOpen)}
|
||||
className={`hidden lg:flex items-center justify-center w-8 h-8 rounded transition-colors ${
|
||||
isAlertsOpen
|
||||
? 'text-yellow-400 bg-yellow-900/30'
|
||||
: 'text-gray-400 hover:text-white hover:bg-gray-800'
|
||||
}`}
|
||||
aria-label="Toggle alerts"
|
||||
title="Toggle Price Alerts"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setIsPaperTradingOpen(!isPaperTradingOpen)}
|
||||
className="hidden lg:flex items-center justify-center w-8 h-8 text-gray-400 hover:text-white hover:bg-gray-800 rounded transition-colors"
|
||||
@ -368,6 +389,18 @@ export default function Trading() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Price Alerts Panel - Desktop */}
|
||||
<div className={`hidden lg:block transition-all duration-300 ${isAlertsOpen ? 'w-80' : 'w-0 overflow-hidden'}`}>
|
||||
{isAlertsOpen && (
|
||||
<div className="h-[600px]">
|
||||
<AlertsPanel
|
||||
symbol={selectedSymbol}
|
||||
currentPrice={currentTicker?.price}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Paper Trading Panel - Desktop */}
|
||||
<div className={`hidden lg:block transition-all duration-300 ${isPaperTradingOpen ? 'w-80' : 'w-0 overflow-hidden'}`}>
|
||||
{isPaperTradingOpen && (
|
||||
|
||||
@ -789,6 +789,167 @@ export async function getLLMAgentHealth(): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Price Alerts API
|
||||
// ============================================================================
|
||||
|
||||
export type AlertCondition = 'above' | 'below' | 'crosses_above' | 'crosses_below';
|
||||
|
||||
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;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface CreateAlertInput {
|
||||
symbol: string;
|
||||
condition: AlertCondition;
|
||||
price: number;
|
||||
note?: string;
|
||||
notifyEmail?: boolean;
|
||||
notifyPush?: boolean;
|
||||
isRecurring?: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateAlertInput {
|
||||
price?: number;
|
||||
note?: string;
|
||||
notifyEmail?: boolean;
|
||||
notifyPush?: boolean;
|
||||
isRecurring?: boolean;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
export interface AlertsFilter {
|
||||
isActive?: boolean;
|
||||
symbol?: string;
|
||||
condition?: AlertCondition;
|
||||
}
|
||||
|
||||
export interface AlertStats {
|
||||
total: number;
|
||||
active: number;
|
||||
triggered: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's 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;
|
||||
|
||||
const response = await api.get('/trading/alerts', { params });
|
||||
return response.data.data || response.data;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch alerts:', error);
|
||||
throw new Error('Failed to fetch alerts');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get alert by ID
|
||||
*/
|
||||
export async function getAlertById(alertId: string): Promise<PriceAlert> {
|
||||
try {
|
||||
const response = await api.get(`/trading/alerts/${alertId}`);
|
||||
return response.data.data || response.data;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch alert:', error);
|
||||
throw new Error('Failed to fetch alert');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new price alert
|
||||
*/
|
||||
export async function createAlert(input: CreateAlertInput): Promise<PriceAlert> {
|
||||
try {
|
||||
const response = await api.post('/trading/alerts', input);
|
||||
return response.data.data || response.data;
|
||||
} catch (error) {
|
||||
console.error('Failed to create alert:', error);
|
||||
throw new Error('Failed to create alert');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an alert
|
||||
*/
|
||||
export async function updateAlert(alertId: string, updates: UpdateAlertInput): Promise<PriceAlert> {
|
||||
try {
|
||||
const response = await api.patch(`/trading/alerts/${alertId}`, updates);
|
||||
return response.data.data || response.data;
|
||||
} catch (error) {
|
||||
console.error('Failed to update alert:', error);
|
||||
throw new Error('Failed to update alert');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an alert
|
||||
*/
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable an alert
|
||||
*/
|
||||
export async function enableAlert(alertId: string): Promise<PriceAlert> {
|
||||
try {
|
||||
const response = await api.post(`/trading/alerts/${alertId}/enable`);
|
||||
return response.data.data || response.data;
|
||||
} catch (error) {
|
||||
console.error('Failed to enable alert:', error);
|
||||
throw new Error('Failed to enable alert');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable an alert
|
||||
*/
|
||||
export async function disableAlert(alertId: string): Promise<PriceAlert> {
|
||||
try {
|
||||
const response = await api.post(`/trading/alerts/${alertId}/disable`);
|
||||
return response.data.data || response.data;
|
||||
} catch (error) {
|
||||
console.error('Failed to disable alert:', error);
|
||||
throw new Error('Failed to disable alert');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get alert statistics
|
||||
*/
|
||||
export async function getAlertStats(): Promise<AlertStats> {
|
||||
try {
|
||||
const response = await api.get('/trading/alerts/stats');
|
||||
return response.data.data || response.data;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch alert stats:', error);
|
||||
throw new Error('Failed to fetch alert stats');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Export
|
||||
// ============================================================================
|
||||
@ -842,6 +1003,16 @@ export const tradingService = {
|
||||
modifyMT4Position,
|
||||
calculatePositionSize,
|
||||
getLLMAgentHealth,
|
||||
|
||||
// Price Alerts
|
||||
getAlerts,
|
||||
getAlertById,
|
||||
createAlert,
|
||||
updateAlert,
|
||||
deleteAlert,
|
||||
enableAlert,
|
||||
disableAlert,
|
||||
getAlertStats,
|
||||
};
|
||||
|
||||
export default tradingService;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user