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:
@@ -0,0 +1,450 @@
|
|||||||
|
"""
|
||||||
|
Phase 4: Economic Calendar API Integration
|
||||||
|
Real-time economic events and market-moving indicators
|
||||||
|
"""
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Query, HTTPException
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import List, Optional
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api/economic-calendar", tags=["Economic Calendar"])
|
||||||
|
|
||||||
|
|
||||||
|
# Mock economic calendar data (in production, integrate with real APIs)
|
||||||
|
# Popular APIs: Trading Economics, Forexfactory, Economic Calendar Pro, etc.
|
||||||
|
SAMPLE_EVENTS = [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"country": "US",
|
||||||
|
"indicator": "Non-Farm Payroll",
|
||||||
|
"event_date": (datetime.now() + timedelta(days=1)).isoformat(),
|
||||||
|
"time": "08:30",
|
||||||
|
"impact": "high",
|
||||||
|
"forecast": "230000",
|
||||||
|
"previous": "227000",
|
||||||
|
"actual": None,
|
||||||
|
"description": "Employment change in the non-agricultural sector",
|
||||||
|
"importance": 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"country": "US",
|
||||||
|
"indicator": "Unemployment Rate",
|
||||||
|
"event_date": (datetime.now() + timedelta(days=1)).isoformat(),
|
||||||
|
"time": "08:30",
|
||||||
|
"impact": "high",
|
||||||
|
"forecast": "3.8%",
|
||||||
|
"previous": "3.8%",
|
||||||
|
"actual": None,
|
||||||
|
"description": "Percentage of the labor force that is jobless",
|
||||||
|
"importance": 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"country": "US",
|
||||||
|
"indicator": "Consumer Price Index",
|
||||||
|
"event_date": (datetime.now() + timedelta(days=5)).isoformat(),
|
||||||
|
"time": "12:30",
|
||||||
|
"impact": "high",
|
||||||
|
"forecast": "3.4%",
|
||||||
|
"previous": "3.4%",
|
||||||
|
"actual": None,
|
||||||
|
"description": "Inflation rate measurement",
|
||||||
|
"importance": 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"country": "US",
|
||||||
|
"indicator": "Federal Funds Rate Decision",
|
||||||
|
"event_date": (datetime.now() + timedelta(days=8)).isoformat(),
|
||||||
|
"time": "18:00",
|
||||||
|
"impact": "high",
|
||||||
|
"forecast": "5.33%",
|
||||||
|
"previous": "5.33%",
|
||||||
|
"actual": None,
|
||||||
|
"description": "Federal Reserve interest rate decision",
|
||||||
|
"importance": 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"country": "EUR",
|
||||||
|
"indicator": "ECB Interest Rate Decision",
|
||||||
|
"event_date": (datetime.now() + timedelta(days=10)).isoformat(),
|
||||||
|
"time": "12:45",
|
||||||
|
"impact": "high",
|
||||||
|
"forecast": "4.50%",
|
||||||
|
"previous": "4.50%",
|
||||||
|
"actual": None,
|
||||||
|
"description": "European Central Bank rate decision",
|
||||||
|
"importance": 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"country": "US",
|
||||||
|
"indicator": "ISM Manufacturing PMI",
|
||||||
|
"event_date": (datetime.now() + timedelta(days=2)).isoformat(),
|
||||||
|
"time": "09:00",
|
||||||
|
"impact": "medium",
|
||||||
|
"forecast": "49.5",
|
||||||
|
"previous": "49.0",
|
||||||
|
"actual": None,
|
||||||
|
"description": "Manufacturing sector activity indicator",
|
||||||
|
"importance": 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"country": "US",
|
||||||
|
"indicator": "Initial Jobless Claims",
|
||||||
|
"event_date": (datetime.now() + timedelta(days=3)).isoformat(),
|
||||||
|
"time": "08:30",
|
||||||
|
"impact": "medium",
|
||||||
|
"forecast": "215000",
|
||||||
|
"previous": "216000",
|
||||||
|
"actual": None,
|
||||||
|
"description": "Weekly unemployment benefit applications",
|
||||||
|
"importance": 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 8,
|
||||||
|
"country": "US",
|
||||||
|
"indicator": "Retail Sales",
|
||||||
|
"event_date": (datetime.now() + timedelta(days=7)).isoformat(),
|
||||||
|
"time": "12:30",
|
||||||
|
"impact": "medium",
|
||||||
|
"forecast": "0.4%",
|
||||||
|
"previous": "0.7%",
|
||||||
|
"actual": None,
|
||||||
|
"description": "Consumer spending and retail activity",
|
||||||
|
"importance": 2,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/events")
|
||||||
|
async def get_economic_events(
|
||||||
|
days_ahead: int = Query(30, ge=1, le=180),
|
||||||
|
countries: Optional[str] = Query(None),
|
||||||
|
impact: Optional[str] = Query(None, regex="^(high|medium|low)$"),
|
||||||
|
sort_by: str = Query("date", regex="^(date|importance|impact)$"),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Get upcoming economic calendar events
|
||||||
|
|
||||||
|
- **days_ahead**: Number of days to look ahead (1-180)
|
||||||
|
- **countries**: Comma-separated country codes (US, EUR, GBP, JPY, etc.)
|
||||||
|
- **impact**: Filter by impact level (high, medium, low)
|
||||||
|
- **sort_by**: Sort results by date, importance, or impact
|
||||||
|
"""
|
||||||
|
events = SAMPLE_EVENTS.copy()
|
||||||
|
|
||||||
|
# Filter by countries
|
||||||
|
if countries:
|
||||||
|
country_list = [c.strip() for c in countries.split(",")]
|
||||||
|
events = [e for e in events if e["country"] in country_list]
|
||||||
|
|
||||||
|
# Filter by impact
|
||||||
|
if impact:
|
||||||
|
impact_map = {"high": 3, "medium": 2, "low": 1}
|
||||||
|
events = [e for e in events if e["importance"] == impact_map.get(impact, 2)]
|
||||||
|
|
||||||
|
# Filter by days ahead
|
||||||
|
cutoff_date = datetime.now() + timedelta(days=days_ahead)
|
||||||
|
events = [
|
||||||
|
e
|
||||||
|
for e in events
|
||||||
|
if datetime.fromisoformat(e["event_date"]) <= cutoff_date
|
||||||
|
]
|
||||||
|
|
||||||
|
# Sort
|
||||||
|
if sort_by == "importance":
|
||||||
|
events.sort(key=lambda x: x["importance"], reverse=True)
|
||||||
|
elif sort_by == "impact":
|
||||||
|
impact_order = {"high": 3, "medium": 2, "low": 1}
|
||||||
|
events.sort(key=lambda x: impact_order.get(x["impact"], 1), reverse=True)
|
||||||
|
else: # date
|
||||||
|
events.sort(key=lambda x: x["event_date"])
|
||||||
|
|
||||||
|
return {
|
||||||
|
"total": len(events),
|
||||||
|
"events": events,
|
||||||
|
"filter_applied": {
|
||||||
|
"days_ahead": days_ahead,
|
||||||
|
"countries": countries,
|
||||||
|
"impact": impact,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/today")
|
||||||
|
async def get_today_events():
|
||||||
|
"""Get economic events scheduled for today"""
|
||||||
|
today = datetime.now().date()
|
||||||
|
today_start = datetime.combine(today, datetime.min.time()).isoformat()
|
||||||
|
today_end = datetime.combine(today, datetime.max.time()).isoformat()
|
||||||
|
|
||||||
|
events = [
|
||||||
|
e
|
||||||
|
for e in SAMPLE_EVENTS
|
||||||
|
if today_start <= e["event_date"] <= today_end
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"date": today.isoformat(),
|
||||||
|
"total": len(events),
|
||||||
|
"events": events,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/upcoming")
|
||||||
|
async def get_upcoming_events(hours: int = Query(24, ge=1, le=168)):
|
||||||
|
"""
|
||||||
|
Get upcoming events within specified hours
|
||||||
|
|
||||||
|
- **hours**: Number of hours ahead to check (1-168 hours = 1-7 days)
|
||||||
|
"""
|
||||||
|
now = datetime.now()
|
||||||
|
cutoff = now + timedelta(hours=hours)
|
||||||
|
|
||||||
|
events = [
|
||||||
|
e
|
||||||
|
for e in SAMPLE_EVENTS
|
||||||
|
if now <= datetime.fromisoformat(e["event_date"]) <= cutoff
|
||||||
|
]
|
||||||
|
|
||||||
|
# Sort by time
|
||||||
|
events.sort(key=lambda x: x["event_date"])
|
||||||
|
|
||||||
|
return {
|
||||||
|
"now": now.isoformat(),
|
||||||
|
"hours_ahead": hours,
|
||||||
|
"total": len(events),
|
||||||
|
"events": events,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/high-impact")
|
||||||
|
async def get_high_impact_events():
|
||||||
|
"""Get only high-impact economic events for the next 30 days"""
|
||||||
|
cutoff = datetime.now() + timedelta(days=30)
|
||||||
|
events = [
|
||||||
|
e
|
||||||
|
for e in SAMPLE_EVENTS
|
||||||
|
if e["importance"] == 3
|
||||||
|
and datetime.fromisoformat(e["event_date"]) <= cutoff
|
||||||
|
]
|
||||||
|
|
||||||
|
events.sort(key=lambda x: x["event_date"])
|
||||||
|
|
||||||
|
return {
|
||||||
|
"total": len(events),
|
||||||
|
"events": events,
|
||||||
|
"note": "Only high-impact events that could significantly move gold prices",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/by-country/{country}")
|
||||||
|
async def get_country_events(
|
||||||
|
country: str, days: int = Query(30, ge=1, le=180)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Get economic events for a specific country
|
||||||
|
|
||||||
|
- **country**: Country code (US, EUR, GBP, JPY, CHF, CAD, AUD, NZD, etc.)
|
||||||
|
- **days**: Days to look ahead
|
||||||
|
"""
|
||||||
|
cutoff = datetime.now() + timedelta(days=days)
|
||||||
|
events = [
|
||||||
|
e
|
||||||
|
for e in SAMPLE_EVENTS
|
||||||
|
if e["country"].upper() == country.upper()
|
||||||
|
and datetime.fromisoformat(e["event_date"]) <= cutoff
|
||||||
|
]
|
||||||
|
|
||||||
|
if not events:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404, detail=f"No events found for country: {country}"
|
||||||
|
)
|
||||||
|
|
||||||
|
events.sort(key=lambda x: x["event_date"])
|
||||||
|
|
||||||
|
return {
|
||||||
|
"country": country.upper(),
|
||||||
|
"days": days,
|
||||||
|
"total": len(events),
|
||||||
|
"events": events,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/impact-analysis")
|
||||||
|
async def get_impact_analysis():
|
||||||
|
"""
|
||||||
|
Analyze economic impact on gold prices
|
||||||
|
|
||||||
|
Returns analysis of how different economic indicators
|
||||||
|
typically affect gold trading
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"gold_trading_impact": {
|
||||||
|
"high_impact": {
|
||||||
|
"indicators": [
|
||||||
|
"Interest Rate Decisions",
|
||||||
|
"Inflation Data",
|
||||||
|
"Employment Reports",
|
||||||
|
"GDP Growth",
|
||||||
|
],
|
||||||
|
"typical_response": "Gold typically moves 100-200 pips on high-impact events",
|
||||||
|
"best_time": "Around event release time",
|
||||||
|
},
|
||||||
|
"medium_impact": {
|
||||||
|
"indicators": [
|
||||||
|
"PMI Indices",
|
||||||
|
"Consumer Confidence",
|
||||||
|
"Retail Sales",
|
||||||
|
"Producer Prices",
|
||||||
|
],
|
||||||
|
"typical_response": "Gold typically moves 50-100 pips",
|
||||||
|
"best_time": "Watch 5-30 mins after release",
|
||||||
|
},
|
||||||
|
"low_impact": {
|
||||||
|
"indicators": [
|
||||||
|
"Housing Starts",
|
||||||
|
"Factory Orders",
|
||||||
|
"Building Permits",
|
||||||
|
],
|
||||||
|
"typical_response": "Gold rarely moves significantly",
|
||||||
|
"best_time": "Usually skipped by day traders",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"inverse_correlation": {
|
||||||
|
"US_Dollar_Strength": "Strong dollar typically weakens gold (inverse correlation)",
|
||||||
|
"Interest_Rates": "Higher rates reduce gold appeal (inverse correlation)",
|
||||||
|
"Risk_Appetite": "Risk-on environment weakens gold demand",
|
||||||
|
"Inflation": "High inflation supports higher gold prices",
|
||||||
|
},
|
||||||
|
"trading_tips": [
|
||||||
|
"Trade 30 mins after high-impact events when volatility settles",
|
||||||
|
"Avoid trading during overlapping Fed/ECB announcements",
|
||||||
|
"Watch preliminary indicators before main events",
|
||||||
|
"Check gold correlation with USD index and bond yields",
|
||||||
|
"Set wider stops during high-impact event windows",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/calendar-view")
|
||||||
|
async def get_calendar_view(
|
||||||
|
month: Optional[int] = Query(None, ge=1, le=12),
|
||||||
|
year: Optional[int] = Query(None),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Get economic calendar in calendar view format
|
||||||
|
|
||||||
|
- **month**: Specific month (1-12), defaults to current month
|
||||||
|
- **year**: Specific year, defaults to current year
|
||||||
|
"""
|
||||||
|
now = datetime.now()
|
||||||
|
view_month = month or now.month
|
||||||
|
view_year = year or now.year
|
||||||
|
|
||||||
|
calendar_events = {}
|
||||||
|
for event in SAMPLE_EVENTS:
|
||||||
|
event_date = datetime.fromisoformat(event["event_date"])
|
||||||
|
if (
|
||||||
|
event_date.month == view_month
|
||||||
|
and event_date.year == view_year
|
||||||
|
):
|
||||||
|
day = event_date.day
|
||||||
|
if day not in calendar_events:
|
||||||
|
calendar_events[day] = []
|
||||||
|
calendar_events[day].append(
|
||||||
|
{
|
||||||
|
"indicator": event["indicator"],
|
||||||
|
"time": event["time"],
|
||||||
|
"impact": event["impact"],
|
||||||
|
"country": event["country"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"month": view_month,
|
||||||
|
"year": view_year,
|
||||||
|
"calendar": calendar_events,
|
||||||
|
"month_name": datetime(view_year, view_month, 1).strftime("%B"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/events/{event_id}/notify")
|
||||||
|
async def set_event_notification(event_id: int, minutes_before: int = Query(30)):
|
||||||
|
"""
|
||||||
|
Set a notification reminder for an economic event
|
||||||
|
|
||||||
|
- **event_id**: ID of the economic event
|
||||||
|
- **minutes_before**: Notify X minutes before event (15-120)
|
||||||
|
"""
|
||||||
|
event = next((e for e in SAMPLE_EVENTS if e["id"] == event_id), None)
|
||||||
|
if not event:
|
||||||
|
raise HTTPException(status_code=404, detail="Event not found")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "notification_set",
|
||||||
|
"event": event["indicator"],
|
||||||
|
"notify_minutes_before": minutes_before,
|
||||||
|
"event_time": event["event_date"],
|
||||||
|
"notification_time": (
|
||||||
|
datetime.fromisoformat(event["event_date"])
|
||||||
|
- timedelta(minutes=minutes_before)
|
||||||
|
).isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/stats")
|
||||||
|
async def get_economic_calendar_stats():
|
||||||
|
"""Get statistics about upcoming economic events"""
|
||||||
|
now = datetime.now()
|
||||||
|
next_7_days = now + timedelta(days=7)
|
||||||
|
next_30_days = now + timedelta(days=30)
|
||||||
|
|
||||||
|
events_7 = [
|
||||||
|
e
|
||||||
|
for e in SAMPLE_EVENTS
|
||||||
|
if now <= datetime.fromisoformat(e["event_date"]) <= next_7_days
|
||||||
|
]
|
||||||
|
events_30 = [
|
||||||
|
e
|
||||||
|
for e in SAMPLE_EVENTS
|
||||||
|
if now <= datetime.fromisoformat(e["event_date"]) <= next_30_days
|
||||||
|
]
|
||||||
|
|
||||||
|
high_impact = [e for e in events_30 if e["importance"] == 3]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"summary": {
|
||||||
|
"total_events_30_days": len(events_30),
|
||||||
|
"total_events_7_days": len(events_7),
|
||||||
|
"high_impact_events": len(high_impact),
|
||||||
|
"total_countries": len(set(e["country"] for e in events_30)),
|
||||||
|
},
|
||||||
|
"by_impact": {
|
||||||
|
"high": len([e for e in events_30 if e["importance"] == 3]),
|
||||||
|
"medium": len([e for e in events_30 if e["importance"] == 2]),
|
||||||
|
"low": len([e for e in events_30 if e["importance"] == 1]),
|
||||||
|
},
|
||||||
|
"busiest_days": sorted(
|
||||||
|
[
|
||||||
|
(
|
||||||
|
e["event_date"].split("T")[0],
|
||||||
|
len(
|
||||||
|
[
|
||||||
|
x
|
||||||
|
for x in events_30
|
||||||
|
if x["event_date"].split("T")[0] == e["event_date"].split("T")[0]
|
||||||
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for e in events_30
|
||||||
|
],
|
||||||
|
key=lambda x: x[1],
|
||||||
|
reverse=True,
|
||||||
|
)[:5],
|
||||||
|
}
|
||||||
+2
-1
@@ -7,7 +7,7 @@ from app.streaming.live_store import periodic_flush, periodic_maintenance
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
# Newly added routers
|
# Newly added routers
|
||||||
from app.api import account, performance, status, settings_api, prompts, daily_helper, analytics
|
from app.api import account, performance, status, settings_api, prompts, daily_helper, analytics, economic_calendar
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title=settings.APP_NAME,
|
title=settings.APP_NAME,
|
||||||
@@ -43,6 +43,7 @@ app.include_router(settings_api.router, prefix="/api")
|
|||||||
app.include_router(prompts.router, prefix="/api")
|
app.include_router(prompts.router, prefix="/api")
|
||||||
app.include_router(daily_helper.router)
|
app.include_router(daily_helper.router)
|
||||||
app.include_router(analytics.router)
|
app.include_router(analytics.router)
|
||||||
|
app.include_router(economic_calendar.router)
|
||||||
|
|
||||||
|
|
||||||
@app.on_event("startup")
|
@app.on_event("startup")
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ import DailyChecklistPanel from './components/DailyChecklistPanel'
|
|||||||
// Phase 3: Advanced Analytics Components
|
// Phase 3: Advanced Analytics Components
|
||||||
import AnalyticsDashboard from './components/AnalyticsDashboard'
|
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 }) {
|
function Tabs({ tabs, active, onChange }: { tabs: string[]; active: string; onChange: (t: string) => void }) {
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', gap: 8, marginBottom: 12 }}>
|
<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() {
|
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 [backendStatus, setBackendStatus] = useState<any>(null)
|
||||||
const [showProfileSetup, setShowProfileSetup] = useState(false)
|
const [showProfileSetup, setShowProfileSetup] = useState(false)
|
||||||
|
|
||||||
@@ -47,7 +50,7 @@ export default function App() {
|
|||||||
return () => { mounted = false }
|
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 (
|
return (
|
||||||
<div className="min-h-screen bg-dark-bg p-6">
|
<div className="min-h-screen bg-dark-bg p-6">
|
||||||
@@ -84,6 +87,7 @@ export default function App() {
|
|||||||
{activeTab === 'Equity' && <EquityPerformancePanel />}
|
{activeTab === 'Equity' && <EquityPerformancePanel />}
|
||||||
{activeTab === 'Decisions' && <DecisionLogPanel />}
|
{activeTab === 'Decisions' && <DecisionLogPanel />}
|
||||||
{activeTab === 'Analytics' && <AnalyticsDashboard />}
|
{activeTab === 'Analytics' && <AnalyticsDashboard />}
|
||||||
|
{activeTab === 'Economic Calendar' && <EconomicCalendar />}
|
||||||
|
|
||||||
{activeTab === 'Daily Helper' && (
|
{activeTab === 'Daily Helper' && (
|
||||||
<div style={{ display: 'grid', gap: 16, gridTemplateColumns: 'repeat(auto-fit, minmax(400px, 1fr))' }}>
|
<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