207 lines
5.0 KiB
Go
207 lines
5.0 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type StockPriceRequest struct {
|
|
Tickers []string `json:"tickers"`
|
|
}
|
|
|
|
type StockPrice struct {
|
|
Ticker string `json:"ticker"`
|
|
USD float64 `json:"usd"`
|
|
EUR float64 `json:"eur"`
|
|
Success bool `json:"success"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
type StockPricesResponse struct {
|
|
Prices map[string]StockPrice `json:"prices"`
|
|
ExchangeRate float64 `json:"exchangeRate"`
|
|
}
|
|
|
|
type YahooChartResponse struct {
|
|
Chart struct {
|
|
Result []struct {
|
|
Meta struct {
|
|
RegularMarketPrice float64 `json:"regularMarketPrice"`
|
|
} `json:"meta"`
|
|
} `json:"result"`
|
|
Error *struct {
|
|
Description string `json:"description"`
|
|
} `json:"error"`
|
|
} `json:"chart"`
|
|
}
|
|
|
|
type ExchangeRateResponse struct {
|
|
Rates map[string]float64 `json:"rates"`
|
|
}
|
|
|
|
func fetchExchangeRate() (float64, error) {
|
|
client := &http.Client{Timeout: 10 * time.Second}
|
|
resp, err := client.Get("https://api.exchangerate-api.com/v4/latest/USD")
|
|
if err != nil {
|
|
return 0.92, fmt.Errorf("failed to fetch exchange rate: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
var rateData ExchangeRateResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&rateData); err != nil {
|
|
return 0.92, fmt.Errorf("failed to decode exchange rate: %w", err)
|
|
}
|
|
|
|
if eurRate, ok := rateData.Rates["EUR"]; ok {
|
|
return eurRate, nil
|
|
}
|
|
|
|
return 0.92, fmt.Errorf("EUR rate not found")
|
|
}
|
|
|
|
func fetchStockPrice(ticker string) (float64, error) {
|
|
client := &http.Client{Timeout: 10 * time.Second}
|
|
|
|
// Try different ticker formats for European stocks
|
|
tickersToTry := []string{ticker}
|
|
|
|
// If ticker doesn't have exchange suffix, try adding common ones
|
|
if !strings.Contains(ticker, ".") {
|
|
tickersToTry = append(tickersToTry,
|
|
ticker+".L", // London Stock Exchange
|
|
ticker+".AS", // Amsterdam
|
|
ticker+".PA", // Paris
|
|
ticker+".DE", // XETRA (Germany)
|
|
ticker+".MI", // Milan
|
|
)
|
|
}
|
|
|
|
var lastErr error
|
|
for _, tryTicker := range tickersToTry {
|
|
// Try Yahoo Finance API
|
|
url := fmt.Sprintf("https://query1.finance.yahoo.com/v8/finance/chart/%s?interval=1d&range=1d", tryTicker)
|
|
req, err := http.NewRequest("GET", url, nil)
|
|
if err != nil {
|
|
lastErr = err
|
|
continue
|
|
}
|
|
|
|
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
lastErr = fmt.Errorf("failed to fetch from Yahoo: %w", err)
|
|
continue
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
lastErr = fmt.Errorf("failed to read response: %w", err)
|
|
continue
|
|
}
|
|
|
|
var chartData YahooChartResponse
|
|
if err := json.Unmarshal(body, &chartData); err != nil {
|
|
lastErr = fmt.Errorf("failed to decode Yahoo response: %w", err)
|
|
continue
|
|
}
|
|
|
|
if chartData.Chart.Error != nil {
|
|
lastErr = fmt.Errorf("Yahoo API error: %s", chartData.Chart.Error.Description)
|
|
continue
|
|
}
|
|
|
|
if len(chartData.Chart.Result) > 0 {
|
|
price := chartData.Chart.Result[0].Meta.RegularMarketPrice
|
|
if price > 0 {
|
|
log.Printf("Successfully fetched price for %s (tried as %s): $%.2f", ticker, tryTicker, price)
|
|
return price, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
if lastErr != nil {
|
|
return 0, fmt.Errorf("price not found after trying all formats: %w", lastErr)
|
|
}
|
|
return 0, fmt.Errorf("price not found in response")
|
|
}
|
|
|
|
func stockPricesHandler(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
var req StockPriceRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Fetch exchange rate
|
|
exchangeRate, err := fetchExchangeRate()
|
|
if err != nil {
|
|
log.Printf("Warning: Using fallback exchange rate: %v", err)
|
|
exchangeRate = 0.92 // Fallback
|
|
}
|
|
|
|
prices := make(map[string]StockPrice)
|
|
|
|
// Fetch prices for each ticker
|
|
for _, ticker := range req.Tickers {
|
|
ticker = strings.TrimSpace(ticker)
|
|
if ticker == "" {
|
|
continue
|
|
}
|
|
|
|
usdPrice, err := fetchStockPrice(ticker)
|
|
if err != nil {
|
|
log.Printf("Error fetching price for %s: %v", ticker, err)
|
|
prices[ticker] = StockPrice{
|
|
Ticker: ticker,
|
|
Success: false,
|
|
Error: err.Error(),
|
|
}
|
|
} else {
|
|
prices[ticker] = StockPrice{
|
|
Ticker: ticker,
|
|
USD: usdPrice,
|
|
EUR: usdPrice * exchangeRate,
|
|
Success: true,
|
|
}
|
|
}
|
|
|
|
// Small delay to avoid rate limiting
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
|
|
response := StockPricesResponse{
|
|
Prices: prices,
|
|
ExchangeRate: exchangeRate,
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(response)
|
|
}
|
|
|
|
func main() {
|
|
// API endpoint
|
|
http.HandleFunc("/api/stock-prices", stockPricesHandler)
|
|
|
|
// Serve static files from /app/static directory
|
|
fs := http.FileServer(http.Dir("/app/static"))
|
|
http.Handle("/", fs)
|
|
|
|
port := ":8080"
|
|
log.Printf("Server starting on port %s", port)
|
|
if err := http.ListenAndServe(port, nil); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|