Restructured
This commit is contained in:
3
backend/go.mod
Normal file
3
backend/go.mod
Normal file
@ -0,0 +1,3 @@
|
||||
module stock-analyzer
|
||||
|
||||
go 1.21
|
||||
180
backend/main.go
Normal file
180
backend/main.go
Normal file
@ -0,0 +1,180 @@
|
||||
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 Yahoo Finance API
|
||||
url := fmt.Sprintf("https://query1.finance.yahoo.com/v8/finance/chart/%s?interval=1d&range=1d", ticker)
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
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 {
|
||||
return 0, fmt.Errorf("failed to fetch from Yahoo: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to read response: %w", err)
|
||||
}
|
||||
|
||||
var chartData YahooChartResponse
|
||||
if err := json.Unmarshal(body, &chartData); err != nil {
|
||||
return 0, fmt.Errorf("failed to decode Yahoo response: %w", err)
|
||||
}
|
||||
|
||||
if chartData.Chart.Error != nil {
|
||||
return 0, fmt.Errorf("Yahoo API error: %s", chartData.Chart.Error.Description)
|
||||
}
|
||||
|
||||
if len(chartData.Chart.Result) > 0 {
|
||||
price := chartData.Chart.Result[0].Meta.RegularMarketPrice
|
||||
if price > 0 {
|
||||
return price, nil
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user