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 @@ 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…

- + - + - - - - - - - - + - - - - - - - - - - - + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
SymbolTicker Asset CcyDate / TimeDate Dir QtyTrade PriceCurr. PriceProceedsComm/FeeBasisRealized P/LMTM P/LCodePrice
NOVOBcStocksDKK2026-02-24 03:05BUY8241.70243.65−1,933.60−10.001,943.600.00+15.60O
NOVOBcStocksDKK2026-02-25 07:10BUY3239.50238.40−718.50−10.00728.500.00−3.30O
CSIQStocksUSD2026-01-20 14:33BUY820.4820.51−163.84−0.38164.220.00+0.24O
PYPLStocksUSD2026-01-20 14:31SELL−355.1855.08165.53−0.35−187.91−22.73+0.29C
RGTI 15JAN27 5.5 POptionsUSD2026-02-24 09:34BUY10.530.5271−53.00−1.0554.050.00−0.29O
@@ -448,11 +363,250 @@
- \ No newline at end of file diff --git a/static/styles.css b/static/styles.css index 3676981..72539d4 100644 --- a/static/styles.css +++ b/static/styles.css @@ -1150,4 +1150,229 @@ nav .section-label { .cards { grid-template-columns: 1fr; } -} \ No newline at end of file +} + /* ── Trades accordion ── */ + .trades-section { + margin-top: 2.5rem; + } + + .trades-toggle { + display: flex; + align-items: center; + justify-content: space-between; + cursor: pointer; + background: none; + border: none; + border-top: 1px solid var(--border, #2a2a3a); + border-bottom: 1px solid var(--border, #2a2a3a); + width: 100%; + padding: 0.85rem 0; + color: inherit; + font-family: 'DM Mono', monospace; + font-size: 0.75rem; + letter-spacing: 0.07em; + text-align: left; + user-select: none; + } + + .trades-toggle:hover .toggle-label { + color: var(--accent, #f59e0b); + } + + .toggle-label { + color: var(--muted, #475569); + transition: color 0.15s; + } + + .toggle-label span { + color: var(--accent, #f59e0b); + } + + .toggle-icon { + color: var(--muted, #475569); + font-size: 1rem; + transition: transform 0.25s ease; + line-height: 1; + } + + .trades-toggle[aria-expanded="true"] .toggle-icon { + transform: rotate(180deg); + } + + .trades-body { + display: grid; + grid-template-rows: 0fr; + transition: grid-template-rows 0.3s ease; + } + + .trades-body.open { + grid-template-rows: 1fr; + } + + .trades-inner { + overflow: hidden; + } + + .trades-filter { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; + padding: 1rem 0 0.75rem; + } + + .filter-btn { + background: none; + border: 1px solid var(--border, #2a2a3a); + border-radius: 4px; + color: var(--muted, #475569); + font-family: 'DM Mono', monospace; + font-size: 0.7rem; + letter-spacing: 0.06em; + padding: 3px 10px; + cursor: pointer; + transition: all 0.15s; + } + + .filter-btn:hover, + .filter-btn.active { + border-color: var(--accent, #f59e0b); + color: var(--accent, #f59e0b); + background: rgba(245,158,11,0.06); + } + + .trades-table-wrap { + overflow-x: auto; + padding-bottom: 1.5rem; + } + + .trades-table { + width: 100%; + border-collapse: collapse; + font-size: 0.78rem; + } + + .trades-table thead tr { + border-bottom: 1px solid var(--border, #2a2a3a); + } + + .trades-table th { + color: var(--muted, #475569); + font-weight: 400; + letter-spacing: 0.07em; + font-size: 0.68rem; + padding: 0.5rem 0.75rem; + text-align: left; + white-space: nowrap; + } + + .trades-table td { + padding: 0.65rem 0.75rem; + border-bottom: 1px solid rgba(42,42,58,0.5); + white-space: nowrap; + color: var(--text, #e2e8f0); + } + + .trades-table tbody tr:last-child td { + border-bottom: none; + } + + .trades-table tbody tr:hover td { + background: rgba(255,255,255,0.02); + } + + .trades-table tr.hidden-row { + display: none; + } + + .dir-buy { color: #4ade80; } + .dir-sell { color: #f87171; } + + .pnl-pos { color: #4ade80; } + .pnl-neg { color: #f87171; } + .pnl-zero{ color: var(--muted, #475569); } + + .trade-code { + display: inline-block; + padding: 1px 6px; + border-radius: 3px; + font-size: 0.65rem; + background: rgba(42,42,58,0.8); + color: var(--muted, #475569); + letter-spacing: 0.05em; + } + + .trade-ticker { + font-weight: 500; + color: #f1f5f9; + } + + .trades-count { + color: var(--muted, #475569); + font-size: 0.68rem; + padding: 0 0 0.5rem; + } + + /* ── Live price styles ── */ + .price-cell { + position: relative; + } + .price-val { + transition: color 0.4s; + } + .price-val.loading { + color: var(--muted, #475569); + } + .price-val.flash-up { + color: #4ade80; + } + .price-val.flash-down { + color: #f87171; + } + .price-status { + display: inline-flex; + align-items: center; + gap: 0.4rem; + font-size: 0.68rem; + color: var(--muted, #475569); + margin-left: 0.5rem; + vertical-align: middle; + } + .price-dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--muted, #475569); + display: inline-block; + } + .price-dot.live { background: #4ade80; box-shadow: 0 0 4px #4ade80; } + .price-dot.error { background: #f87171; } + .price-dot.loading { + background: #f59e0b; + animation: pulse 1s ease-in-out infinite; + } + @keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.3; } + } + .refresh-btn { + background: none; + border: 1px solid var(--border, #2a2a3a); + border-radius: 4px; + color: var(--muted, #475569); + font-family: 'DM Mono', monospace; + font-size: 0.68rem; + padding: 2px 8px; + cursor: pointer; + letter-spacing: 0.05em; + transition: all 0.15s; + } + .refresh-btn:hover { + border-color: var(--accent, #f59e0b); + color: var(--accent, #f59e0b); + } + .section-header-row { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 0.6rem; + } \ No newline at end of file