package main import ( "encoding/csv" "encoding/json" "flag" "fmt" "log" "net/http" "strings" "sync" "time" ) // ---------- HTTP Handlers ---------- func HelloHandler(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/hello" { http.Error(w, "404 not found", http.StatusNotFound) return } if r.Method != "GET" { http.Error(w, "method is not supported", http.StatusNotFound) return } fmt.Fprintf(w, "Hello You") } func Home(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/index.html") } func Portfolio(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/portfolio.html") } func Infra(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/infra.html") } func Cyber(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/cyber.html") } func About(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/about.html") } func GitPage(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/gitpage.html") } func GitPageTicTacToe(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/gitpages/TicTacToe.html") } func Styles(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/styles.css") } // TradesHandler handles POST /trades // It accepts a CSV file upload (field name: "file") and returns enriched trades as JSON. // // Example curl: // // curl -X POST http://localhost:8081/trades -F "file=@trades.csv" func TradesHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "only POST is supported", http.StatusMethodNotAllowed) return } // Parse multipart form (max 10MB) if err := r.ParseMultipartForm(10 << 20); err != nil { http.Error(w, "failed to parse form: "+err.Error(), http.StatusBadRequest) return } file, _, err := r.FormFile("Trades.csc") if err != nil { http.Error(w, `missing "file" field in form-data`, http.StatusBadRequest) return } defer file.Close() // Parse CSV reader := csv.NewReader(file) reader.LazyQuotes = true reader.TrimLeadingSpace = true records, err := reader.ReadAll() if err != nil { http.Error(w, "invalid CSV: "+err.Error(), http.StatusBadRequest) return } // Use most recent weekday as price date priceDate := lastWeekday(time.Now().UTC().AddDate(0, 0, -1)) // Enrich and return as JSON enriched, err := enrichRecords(records, priceDate) if err != nil { http.Error(w, "enrichment failed: "+err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(enriched) } // ---------- Main ---------- func main() { port := flag.String("port", "8081", "port to listen on") flag.Parse() http.HandleFunc("/styles.css", Styles) http.HandleFunc("/", About) //http.HandleFunc("/portfolio", Portfolio) http.HandleFunc("/infra", Infra) http.HandleFunc("/cyber", Cyber) //http.HandleFunc("/gitpage", GitPage) //http.HandleFunc("/gitpage/TicTacToe", GitPageTicTacToe) //http.HandleFunc("/trades", TradesHandler) //http.HandleFunc("/api/prices", PricesHandler) fmt.Printf("running on http://localhost:%s/\n", *port) if err := http.ListenAndServe(":"+*port, nil); err != nil { log.Fatal(err) } } // ---------- Trade type ---------- type Trade struct { DataDiscriminator string `json:"data_discriminator"` AssetCategory string `json:"asset_category"` Currency string `json:"currency"` Symbol string `json:"symbol"` DateTime string `json:"date_time"` Quantity string `json:"quantity"` TradePrice string `json:"trade_price"` CurrentPrice float64 `json:"current_price"` // enriched from EOD Proceeds string `json:"proceeds"` CommFee string `json:"comm_fee"` Basis string `json:"basis"` RealizedPL string `json:"realized_pl"` MTMLPL string `json:"mtm_pl"` Code string `json:"code"` } // ---------- Enrich logic ---------- func enrichRecords(records [][]string, priceDate time.Time) ([]Trade, error) { // Collect unique tickers from Data/Order rows type key struct{ symbol, currency string } needed := map[key]struct{}{} for _, row := range records { if len(row) <= colCPrice { continue } if row[colType] == "Trades" && row[colHeader] == "Data" && row[colDataDisc] == "Order" { sym := strings.TrimSpace(row[colSymbol]) cur := strings.TrimSpace(row[colCurrency]) if t := toYahooTicker(sym, cur); t != "" { needed[key{t, cur}] = struct{}{} } } } // Fetch prices concurrently prices := map[string]float64{} var mu sync.Mutex var wg sync.WaitGroup for k := range needed { wg.Add(1) go func(ticker string) { defer wg.Done() eod, err := FetchEOD(ticker, priceDate) mu.Lock() defer mu.Unlock() if err != nil { log.Printf("[warn] %s: %v", ticker, err) } else { prices[ticker] = eod.Close } }(k.symbol) } wg.Wait() // Build response var trades []Trade for _, row := range records { if len(row) <= colCPrice { continue } if row[colType] != "Trades" || row[colHeader] != "Data" || row[colDataDisc] != "Order" { continue } sym := strings.TrimSpace(row[colSymbol]) cur := strings.TrimSpace(row[colCurrency]) ticker := toYahooTicker(sym, cur) var currentPrice float64 if ticker != "" { currentPrice = prices[ticker] } t := Trade{ DataDiscriminator: strings.TrimSpace(row[colDataDisc]), AssetCategory: strings.TrimSpace(row[colAssetCategory]), Currency: cur, Symbol: sym, DateTime: safeCol(row, colDateTime), Quantity: safeCol(row, colQuantity), TradePrice: safeCol(row, colTPrice), CurrentPrice: currentPrice, Proceeds: safeCol(row, 10), CommFee: safeCol(row, 11), Basis: safeCol(row, 12), RealizedPL: safeCol(row, 13), MTMLPL: safeCol(row, 14), Code: safeCol(row, 15), } trades = append(trades, t) } return trades, nil } func safeCol(row []string, i int) string { if i < len(row) { return strings.TrimSpace(row[i]) } return "" } func lastWeekday(t time.Time) time.Time { for t.Weekday() == time.Saturday || t.Weekday() == time.Sunday { t = t.AddDate(0, 0, -1) } return t } const ( colType = 0 colHeader = 1 colDataDisc = 2 colAssetCategory = 3 colCurrency = 4 colSymbol = 5 colDateTime = 6 colQuantity = 7 colTPrice = 8 colCPrice = 9 ) var tickerOverrides = map[string]string{ "NOVOBc": "NOVO-B.CO", } func toYahooTicker(ibkrSymbol, currency string) string { if override, ok := tickerOverrides[ibkrSymbol]; ok { return override } if strings.Contains(ibkrSymbol, " ") { return "" // skip options } return ibkrSymbol } type EODPrice struct { Ticker string Date string Open float64 High float64 Low float64 Close float64 Currency string LatestPrice float64 // regularMarketPrice from meta — most recent quote LatestTime time.Time PreviousClose float64 ChangePercent float64 } func FetchEOD(ticker string, date time.Time) (*EODPrice, error) { start := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, time.UTC) end := start.Add(24 * time.Hour) url := fmt.Sprintf( "https://query1.finance.yahoo.com/v8/finance/chart/%s?period1=%d&period2=%d&interval=1d", ticker, start.Unix(), end.Unix(), ) req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, err } req.Header.Set("User-Agent", "Mozilla/5.0") resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() var payload struct { Chart struct { Result []struct { Meta struct { Currency string `json:"currency"` RegularMarketPrice float64 `json:"regularMarketPrice"` RegularMarketTime int64 `json:"regularMarketTime"` ChartPreviousClose float64 `json:"chartPreviousClose"` RegularMarketChangePercent float64 `json:"regularMarketChangePercent"` } `json:"meta"` Indicators struct { Quote []struct { Open []float64 `json:"open"` High []float64 `json:"high"` Low []float64 `json:"low"` Close []float64 `json:"close"` } `json:"quote"` } `json:"indicators"` } `json:"result"` Error *struct { Description string `json:"description"` } `json:"error"` } `json:"chart"` } if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil { return nil, err } if payload.Chart.Error != nil { return nil, fmt.Errorf("yahoo: %s", payload.Chart.Error.Description) } if len(payload.Chart.Result) == 0 { return nil, fmt.Errorf("no data for %s on %s", ticker, date.Format("2006-01-02")) } r := payload.Chart.Result[0] q := r.Indicators.Quote[0] if len(q.Close) == 0 || q.Close[0] == 0 { return nil, fmt.Errorf("no close price for %s on %s", ticker, date.Format("2006-01-02")) } return &EODPrice{ Ticker: ticker, Date: date.Format("2006-01-02"), Open: q.Open[0], High: q.High[0], Low: q.Low[0], Close: q.Close[0], Currency: r.Meta.Currency, LatestPrice: r.Meta.RegularMarketPrice, LatestTime: time.Unix(r.Meta.RegularMarketTime, 0).UTC(), PreviousClose: r.Meta.ChartPreviousClose, ChangePercent: r.Meta.RegularMarketChangePercent, }, nil } func PricesHandler(w http.ResponseWriter, r *http.Request) { var req struct { Tickers []string `json:"tickers"` } json.NewDecoder(r.Body).Decode(&req) priceDate := lastWeekday(time.Now().UTC().AddDate(0, 0, -1)) var results []EODPrice for _, t := range req.Tickers { eod, err := FetchEOD(t, priceDate) if err == nil { results = append(results, *eod) } } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(results) }