Phase 4: Economic Calendar API Integration
Implemented comprehensive economic calendar system for trading event alerts:
Backend (economic_calendar.py):
- GET /api/economic-calendar/events: Fetch events by days, countries, impact level
- GET /api/economic-calendar/today: Get today's scheduled events
- GET /api/economic-calendar/upcoming: Events within X hours (1-168)
- GET /api/economic-calendar/high-impact: Only critical events (next 30 days)
- GET /api/economic-calendar/by-country/{country}: Country-specific events
- GET /api/economic-calendar/impact-analysis: Gold trading impact analysis
- GET /api/economic-calendar/calendar-view: Calendar view with events by day
- POST /api/economic-calendar/events/{event_id}/notify: Set reminder notifications
- GET /api/economic-calendar/stats: Event statistics and busiest days
Features:
- Sample economic events: NFP, CPI, Unemployment, Fed Rate Decision, ECB Rate
- Impact levels: High/Medium/Low with color coding
- Forecast, previous, and actual values tracking
- Event filtering by country, impact, and days ahead
- Multiple sort options: date, importance, impact
- Notifications 15-120 minutes before events
- Gold trading correlation analysis
- Statistics for 7-day and 30-day windows
Frontend (EconomicCalendar.tsx):
- Calendar overview with event statistics
- Upcoming and high-impact event tabs
- Country-based filtering
- Impact-based color coding (red/yellow/blue)
- Event details: forecast, previous, actual values
- Trading tips for different event types
- Event time display with timezone consideration
- Correlation guidance (USD inverse, rates inverse)
- Visual indicators for pending/actual events
Integration:
- Registered economic_calendar router in main.py
- Added EconomicCalendar tab to App.tsx
- Integrated with navigation system
- Full TypeScript support
Note: Currently uses mock data. In production, integrate with:
- Trading Economics API
- Forexfactory Calendar
- Economic Calendar Pro
- OANDA Calendar
This commit is contained in:
@@ -17,6 +17,9 @@ import DailyChecklistPanel from './components/DailyChecklistPanel'
|
||||
// Phase 3: Advanced Analytics Components
|
||||
import AnalyticsDashboard from './components/AnalyticsDashboard'
|
||||
|
||||
// Phase 4: Economic Calendar & Advanced Features
|
||||
import EconomicCalendar from './components/EconomicCalendar'
|
||||
|
||||
function Tabs({ tabs, active, onChange }: { tabs: string[]; active: string; onChange: (t: string) => void }) {
|
||||
return (
|
||||
<div style={{ display: 'flex', gap: 8, marginBottom: 12 }}>
|
||||
@@ -30,7 +33,7 @@ function Tabs({ tabs, active, onChange }: { tabs: string[]; active: string; onCh
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const [activeTab, setActiveTab] = useState<'Live' | 'Account' | 'Equity' | 'Decisions' | 'Analytics' | 'Settings' | 'Prompts' | 'Daily Helper'>('Live')
|
||||
const [activeTab, setActiveTab] = useState<'Live' | 'Account' | 'Equity' | 'Decisions' | 'Analytics' | 'Economic Calendar' | 'Settings' | 'Prompts' | 'Daily Helper'>('Live')
|
||||
const [backendStatus, setBackendStatus] = useState<any>(null)
|
||||
const [showProfileSetup, setShowProfileSetup] = useState(false)
|
||||
|
||||
@@ -47,7 +50,7 @@ export default function App() {
|
||||
return () => { mounted = false }
|
||||
}, [])
|
||||
|
||||
const tabs = ['Live', 'Account', 'Equity', 'Decisions', 'Analytics', 'Daily Helper', 'Settings', 'Prompts']
|
||||
const tabs = ['Live', 'Account', 'Equity', 'Decisions', 'Analytics', 'Economic Calendar', 'Daily Helper', 'Settings', 'Prompts']
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-dark-bg p-6">
|
||||
@@ -84,6 +87,7 @@ export default function App() {
|
||||
{activeTab === 'Equity' && <EquityPerformancePanel />}
|
||||
{activeTab === 'Decisions' && <DecisionLogPanel />}
|
||||
{activeTab === 'Analytics' && <AnalyticsDashboard />}
|
||||
{activeTab === 'Economic Calendar' && <EconomicCalendar />}
|
||||
|
||||
{activeTab === 'Daily Helper' && (
|
||||
<div style={{ display: 'grid', gap: 16, gridTemplateColumns: 'repeat(auto-fit, minmax(400px, 1fr))' }}>
|
||||
|
||||
@@ -0,0 +1,307 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Calendar, AlertTriangle, TrendingUp, Clock, Globe } from 'lucide-react';
|
||||
import axios from 'axios';
|
||||
|
||||
interface EconomicEvent {
|
||||
id: number;
|
||||
country: string;
|
||||
indicator: string;
|
||||
event_date: string;
|
||||
time: string;
|
||||
impact: 'high' | 'medium' | 'low';
|
||||
forecast: string;
|
||||
previous: string;
|
||||
actual: string | null;
|
||||
description: string;
|
||||
importance: number;
|
||||
}
|
||||
|
||||
interface EventStats {
|
||||
total_events_30_days: number;
|
||||
total_events_7_days: number;
|
||||
high_impact_events: number;
|
||||
total_countries: number;
|
||||
}
|
||||
|
||||
export default function EconomicCalendar() {
|
||||
const [events, setEvents] = useState<EconomicEvent[]>([]);
|
||||
const [highImpactEvents, setHighImpactEvents] = useState<EconomicEvent[]>([]);
|
||||
const [stats, setStats] = useState<EventStats | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activeTab, setActiveTab] = useState<'upcoming' | 'today' | 'high-impact'>('upcoming');
|
||||
const [selectedCountry, setSelectedCountry] = useState<string | null>(null);
|
||||
|
||||
// Fetch events and stats
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// Fetch upcoming events
|
||||
const upcomingResponse = await axios.get('/api/economic-calendar/events', {
|
||||
params: { days_ahead: 30, sort_by: 'date' },
|
||||
});
|
||||
setEvents(upcomingResponse.data.events || []);
|
||||
|
||||
// Fetch high impact events
|
||||
const highImpactResponse = await axios.get('/api/economic-calendar/high-impact');
|
||||
setHighImpactEvents(highImpactResponse.data.events || []);
|
||||
|
||||
// Fetch stats
|
||||
const statsResponse = await axios.get('/api/economic-calendar/stats');
|
||||
setStats(statsResponse.data.summary || null);
|
||||
} catch (error) {
|
||||
console.error('Error fetching economic calendar data:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const getImpactColor = (impact: string): string => {
|
||||
switch (impact) {
|
||||
case 'high':
|
||||
return 'bg-red-900 text-red-300 border-red-700';
|
||||
case 'medium':
|
||||
return 'bg-yellow-900 text-yellow-300 border-yellow-700';
|
||||
default:
|
||||
return 'bg-blue-900 text-blue-300 border-blue-700';
|
||||
}
|
||||
};
|
||||
|
||||
const getImpactBorder = (impact: string): string => {
|
||||
switch (impact) {
|
||||
case 'high':
|
||||
return 'border-l-4 border-red-500';
|
||||
case 'medium':
|
||||
return 'border-l-4 border-yellow-500';
|
||||
default:
|
||||
return 'border-l-4 border-blue-500';
|
||||
}
|
||||
};
|
||||
|
||||
const formatDateTime = (isoDate: string, time: string): string => {
|
||||
const date = new Date(isoDate);
|
||||
return `${date.toLocaleDateString()} at ${time}`;
|
||||
};
|
||||
|
||||
const displayedEvents = activeTab === 'high-impact' ? highImpactEvents : events;
|
||||
const filteredEvents = selectedCountry
|
||||
? displayedEvents.filter((e) => e.country === selectedCountry)
|
||||
: displayedEvents;
|
||||
|
||||
const countries = Array.from(new Set(events.map((e) => e.country))).sort();
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="card">
|
||||
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||
<Calendar className="w-5 h-5 text-blue-500" />
|
||||
Economic Calendar
|
||||
</h3>
|
||||
<div className="text-center text-gray-400 py-8">Loading calendar...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Stats Overview */}
|
||||
{stats && (
|
||||
<div className="card">
|
||||
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||
<TrendingUp className="w-5 h-5 text-blue-500" />
|
||||
Calendar Overview (Next 30 Days)
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
<div className="bg-dark-bg rounded-lg p-3 border border-dark-border">
|
||||
<p className="text-xs text-gray-400 mb-1">Total Events</p>
|
||||
<p className="text-2xl font-bold text-blue-500">{stats.total_events_30_days}</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-dark-bg rounded-lg p-3 border border-dark-border">
|
||||
<p className="text-xs text-gray-400 mb-1">High Impact</p>
|
||||
<p className="text-2xl font-bold text-red-500">{stats.high_impact_events}</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-dark-bg rounded-lg p-3 border border-dark-border">
|
||||
<p className="text-xs text-gray-400 mb-1">Next 7 Days</p>
|
||||
<p className="text-2xl font-bold text-yellow-500">{stats.total_events_7_days}</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-dark-bg rounded-lg p-3 border border-dark-border">
|
||||
<p className="text-xs text-gray-400 mb-1">Countries</p>
|
||||
<p className="text-2xl font-bold text-green-500">{stats.total_countries}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main Calendar */}
|
||||
<div className="card">
|
||||
<div className="mb-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold flex items-center gap-2">
|
||||
<Calendar className="w-5 h-5 text-blue-500" />
|
||||
Economic Events
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex gap-2 mb-4">
|
||||
<button
|
||||
onClick={() => setActiveTab('upcoming')}
|
||||
className={`px-4 py-2 rounded-lg font-medium transition ${
|
||||
activeTab === 'upcoming'
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-dark-bg text-gray-400 hover:text-gray-200 border border-dark-border'
|
||||
}`}
|
||||
>
|
||||
Upcoming ({events.length})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('high-impact')}
|
||||
className={`px-4 py-2 rounded-lg font-medium transition ${
|
||||
activeTab === 'high-impact'
|
||||
? 'bg-red-600 text-white'
|
||||
: 'bg-dark-bg text-gray-400 hover:text-gray-200 border border-dark-border'
|
||||
}`}
|
||||
>
|
||||
<AlertTriangle className="w-4 h-4 inline mr-2" />
|
||||
High Impact ({highImpactEvents.length})
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Country Filter */}
|
||||
<div className="mb-4">
|
||||
<label className="text-sm text-gray-400 mb-2 block">Filter by Country:</label>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<button
|
||||
onClick={() => setSelectedCountry(null)}
|
||||
className={`px-3 py-1 rounded text-sm transition ${
|
||||
selectedCountry === null
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-dark-bg text-gray-400 hover:text-gray-200 border border-dark-border'
|
||||
}`}
|
||||
>
|
||||
All Countries
|
||||
</button>
|
||||
{countries.map((country) => (
|
||||
<button
|
||||
key={country}
|
||||
onClick={() => setSelectedCountry(selectedCountry === country ? null : country)}
|
||||
className={`px-3 py-1 rounded text-sm transition ${
|
||||
selectedCountry === country
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-dark-bg text-gray-400 hover:text-gray-200 border border-dark-border'
|
||||
}`}
|
||||
>
|
||||
{country}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Events List */}
|
||||
<div className="space-y-3 max-h-96 overflow-y-auto">
|
||||
{filteredEvents.length > 0 ? (
|
||||
filteredEvents.map((event) => (
|
||||
<div
|
||||
key={event.id}
|
||||
className={`bg-dark-bg rounded-lg p-4 border ${getImpactBorder(event.impact)}`}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h4 className="font-semibold text-gray-200">{event.indicator}</h4>
|
||||
<span
|
||||
className={`text-xs px-2 py-1 rounded border ${getImpactColor(event.impact)}`}
|
||||
>
|
||||
{event.impact.toUpperCase()}
|
||||
</span>
|
||||
<span className="text-xs bg-gray-700 text-gray-300 px-2 py-1 rounded">
|
||||
<Globe className="w-3 h-3 inline mr-1" />
|
||||
{event.country}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-400 mb-2">{event.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-3 mb-3 text-sm">
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 mb-1">Forecast</p>
|
||||
<p className="font-semibold text-blue-400">{event.forecast}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 mb-1">Previous</p>
|
||||
<p className="font-semibold text-gray-300">{event.previous}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 mb-1">Actual</p>
|
||||
<p className={`font-semibold ${event.actual ? 'text-green-400' : 'text-gray-500'}`}>
|
||||
{event.actual || 'Pending'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 text-xs text-gray-400 pt-2 border-t border-dark-border">
|
||||
<Clock className="w-4 h-4" />
|
||||
<span>{formatDateTime(event.event_date, event.time)}</span>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p className="text-center text-gray-400 py-4">No events found</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Trading Tips */}
|
||||
<div className="card">
|
||||
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||
<AlertTriangle className="w-5 h-5 text-yellow-500" />
|
||||
Gold Trading Tips During Economic Events
|
||||
</h3>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="bg-red-900 bg-opacity-20 border border-red-800 rounded-lg p-3">
|
||||
<p className="text-sm text-red-400 font-semibold mb-1">🔴 High-Impact Events</p>
|
||||
<p className="text-xs text-gray-300">
|
||||
Gold typically moves 100-200 pips. Set wider stops and consider doubling
|
||||
position size around event release time.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-yellow-900 bg-opacity-20 border border-yellow-800 rounded-lg p-3">
|
||||
<p className="text-sm text-yellow-400 font-semibold mb-1">🟡 Medium-Impact Events</p>
|
||||
<p className="text-xs text-gray-300">
|
||||
Gold typically moves 50-100 pips. Wait 5-30 mins after release before
|
||||
trading to let volatility settle.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-900 bg-opacity-20 border border-blue-800 rounded-lg p-3">
|
||||
<p className="text-sm text-blue-400 font-semibold mb-1">🔵 Key Correlations</p>
|
||||
<p className="text-xs text-gray-300">
|
||||
Strong USD weakens gold (inverse). High rates reduce gold appeal. Watch
|
||||
DXY and bond yields for context.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-green-900 bg-opacity-20 border border-green-800 rounded-lg p-3">
|
||||
<p className="text-sm text-green-400 font-semibold mb-1">✅ Best Practice</p>
|
||||
<p className="text-xs text-gray-300">
|
||||
Always check: 1) Event importance 2) USD impact 3) Historical volatility
|
||||
4) Current gold sentiment before trading.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user