diff --git a/examples/fxmacrodata_calendar/main.go b/examples/fxmacrodata_calendar/main.go new file mode 100644 index 00000000..4557c009 --- /dev/null +++ b/examples/fxmacrodata_calendar/main.go @@ -0,0 +1,88 @@ +// Copyright (c) 2021-2026 Onur Cinar. +// The source code is provided under GNU AGPLv3 License. +// https://github.com/cinar/indicator + +// Package main provides a self-contained example of fetching and filtering +// macroeconomic announcement events from the FXMacroData public release-calendar endpoint. +package main + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + "sort" +) + +type calendarResponse struct { + Data []calendarEvent `json:"data"` +} + +type calendarEvent struct { + Name string `json:"name"` + Date string `json:"date"` + AnnouncementDatetimeUTC string `json:"announcement_datetime_utc"` + MarketTier int `json:"market_tier"` + TopTierForCurrency bool `json:"top_tier_for_currency"` +} + +func main() { + events, err := fetchCalendar("USD", "2026-07-01", "2026-07-20") + if err != nil { + panic(err) + } + + fmt.Println("Top-tier USD macro blackout dates:") + for _, event := range topTierEvents(events) { + fmt.Printf(" %s: %s\n", eventDate(event), event.Name) + } +} + +var apiHost = "https://fxmacrodata.com" + +func fetchCalendar(currency, startDate, endDate string) ([]calendarEvent, error) { + endpoint := fmt.Sprintf("%s/api/v1/calendar/%s", apiHost, currency) + params := url.Values{} + params.Set("start_date", startDate) + params.Set("end_date", endDate) + + res, err := http.Get(endpoint + "?" + params.Encode()) + if err != nil { + return nil, err + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return nil, fmt.Errorf("request failed with %s", res.Status) + } + + var payload calendarResponse + if err := json.NewDecoder(res.Body).Decode(&payload); err != nil { + return nil, err + } + return payload.Data, nil +} + +func topTierEvents(events []calendarEvent) []calendarEvent { + filtered := make([]calendarEvent, 0) + for _, event := range events { + if event.TopTierForCurrency || event.MarketTier == 1 { + filtered = append(filtered, event) + } + } + sort.Slice(filtered, func(i, j int) bool { + return eventDate(filtered[i]) < eventDate(filtered[j]) + }) + return filtered +} + +func eventDate(event calendarEvent) string { + value := event.AnnouncementDatetimeUTC + if value == "" { + value = event.Date + } + if len(value) > 10 { + return value[:10] + } + return value +} diff --git a/examples/fxmacrodata_calendar/main_test.go b/examples/fxmacrodata_calendar/main_test.go new file mode 100644 index 00000000..56b6cb8f --- /dev/null +++ b/examples/fxmacrodata_calendar/main_test.go @@ -0,0 +1,171 @@ +// Copyright (c) 2021-2026 Onur Cinar. +// The source code is provided under GNU AGPLv3 License. +// https://github.com/cinar/indicator + +package main + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" +) + +func TestEventDate(t *testing.T) { + tests := []struct { + event calendarEvent + expected string + }{ + { + event: calendarEvent{ + AnnouncementDatetimeUTC: "2026-07-14T12:30:00Z", + }, + expected: "2026-07-14", + }, + { + event: calendarEvent{ + Date: "2026-07-15", + }, + expected: "2026-07-15", + }, + { + event: calendarEvent{ + Date: "2026", + }, + expected: "2026", + }, + } + + for _, test := range tests { + actual := eventDate(test.event) + if actual != test.expected { + t.Errorf("actual %s expected %s", actual, test.expected) + } + } +} + +func TestTopTierEvents(t *testing.T) { + events := []calendarEvent{ + { + Name: "Low Tier Event", + Date: "2026-07-15", + MarketTier: 3, + }, + { + Name: "High Tier Event 1", + Date: "2026-07-16", + MarketTier: 1, + }, + { + Name: "Top Tier Currency Event", + Date: "2026-07-14", + MarketTier: 2, + TopTierForCurrency: true, + }, + } + + filtered := topTierEvents(events) + + if len(filtered) != 2 { + t.Fatalf("expected 2 events, got %d", len(filtered)) + } + + // Verify sorting and filtering + if filtered[0].Name != "Top Tier Currency Event" { + t.Errorf("expected first event to be Top Tier Currency Event, got %s", filtered[0].Name) + } + if filtered[1].Name != "High Tier Event 1" { + t.Errorf("expected second event to be High Tier Event 1, got %s", filtered[1].Name) + } +} + +func TestFetchCalendar(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/v1/calendar/USD" { + t.Errorf("unexpected path: %s", r.URL.Path) + } + if r.URL.Query().Get("start_date") != "2026-07-01" { + t.Errorf("unexpected start_date: %s", r.URL.Query().Get("start_date")) + } + if r.URL.Query().Get("end_date") != "2026-07-20" { + t.Errorf("unexpected end_date: %s", r.URL.Query().Get("end_date")) + } + + response := calendarResponse{ + Data: []calendarEvent{ + { + Name: "Core Inflation", + Date: "2026-07-14", + MarketTier: 1, + }, + }, + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(response) + })) + defer server.Close() + + oldHost := apiHost + apiHost = server.URL + defer func() { apiHost = oldHost }() + + events, err := fetchCalendar("USD", "2026-07-01", "2026-07-20") + if err != nil { + t.Fatalf("failed to fetch calendar: %v", err) + } + + if len(events) != 1 { + t.Fatalf("expected 1 event, got %d", len(events)) + } + + if events[0].Name != "Core Inflation" { + t.Errorf("expected Core Inflation, got %s", events[0].Name) + } +} + +func TestFetchCalendarError(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + })) + defer server.Close() + + oldHost := apiHost + apiHost = server.URL + defer func() { apiHost = oldHost }() + + _, err := fetchCalendar("USD", "2026-07-01", "2026-07-20") + if err == nil { + t.Fatal("expected error, got nil") + } +} + +func TestMainFunction(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + response := calendarResponse{ + Data: []calendarEvent{ + { + Name: "Core Inflation", + Date: "2026-07-14", + MarketTier: 1, + }, + }, + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(response) + })) + defer server.Close() + + oldHost := apiHost + apiHost = server.URL + defer func() { apiHost = oldHost }() + + // Verify main runs without panic when API returns successful result + defer func() { + if r := recover(); r != nil { + t.Errorf("main panicked: %v", r) + } + }() + main() +}