diff --git a/main.go b/main.go index 689651f..1bbcdcf 100644 --- a/main.go +++ b/main.go @@ -1,15 +1,10 @@ package main import ( - "encoding/csv" - "encoding/json" "flag" "fmt" "log" "net/http" - "strings" - "sync" - "time" ) // ---------- HTTP Handlers ---------- @@ -26,69 +21,20 @@ func HelloHandler(w http.ResponseWriter, r *http.Request) { 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 Home(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/index.html") } func Infra(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/infra.html") } func Finance(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/finance.html") } func Cinema(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/cinema.html") } func Cyber(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/cyber.html") } func Engine(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/engine.html") } -func About(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/about.html") } +func Portfolio(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, "./static/portfolio.html") +} +func About(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/about.html") } func Styles(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/styles.css") } func Styles2(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/styles2.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() { @@ -110,6 +56,8 @@ func main() { http.HandleFunc("/cinema", Cinema) http.HandleFunc("/cyber", Cyber) http.HandleFunc("/engine", Engine) + http.HandleFunc("/portfolio", Portfolio) + http.HandleFunc("/git", func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "https://git.samantha42.xyz", http.StatusFound) }) @@ -119,255 +67,3 @@ func main() { 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) -} diff --git a/site b/site index 59fd617..b6bb25a 100755 Binary files a/site and b/site differ diff --git a/static/about.html b/static/about.html index 622bfa1..374c2cc 100644 --- a/static/about.html +++ b/static/about.html @@ -14,11 +14,12 @@ samantha_vero_friis
diff --git a/static/portfolio.html b/static/portfolio.html index 0b1d71b..6aaa178 100644 --- a/static/portfolio.html +++ b/static/portfolio.html @@ -7,231 +7,199 @@ @@ -343,97 +311,44 @@10 trades
+loading…
| Symbol | +Ticker | Asset | Ccy | -Date / Time | +Date | Dir | Qty | -Trade Price | -Curr. Price | -Proceeds | -Comm/Fee | -Basis | -Realized P/L | -MTM P/L | -Code | +Price |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| NOVOBc | -Stocks | DKK | -2026-02-24 03:05 | -BUY | -8 | 241.70 | 243.65 | -−1,933.60 | −10.00 | 1,943.60 | -0.00 | +15.60 | -O | + +|||
| + | + | + | + | + | + | |||||||||||
| NOVOBc | -Stocks | DKK | -2026-02-25 07:10 | -BUY | -3 | 239.50 | 238.40 | -−718.50 | −10.00 | 728.50 | -0.00 | −3.30 | -O | -|||
| CSIQ | -Stocks | USD | -2026-01-20 14:33 | -BUY | -8 | 20.48 | 20.51 | -−163.84 | −0.38 | 164.22 | -0.00 | +0.24 | -O | -|||
| PYPL | -Stocks | USD | -2026-01-20 14:31 | -SELL | -−3 | 55.18 | 55.08 | -165.53 | −0.35 | −187.91 | -−22.73 | +0.29 | -C | -|||
| RGTI 15JAN27 5.5 P | -Options | USD | -2026-02-24 09:34 | -BUY | -1 | 0.53 | 0.5271 | -−53.00 | −1.05 | 54.05 | -0.00 | −0.29 | -O | -