first commit of restructured project
This commit is contained in:
48
trahn-trade-backend/internal/api/grid_routes.go
Normal file
48
trahn-trade-backend/internal/api/grid_routes.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type gridCurrentResponse struct {
|
||||
BasePrice *float64 `json:"basePrice"`
|
||||
Grid json.RawMessage `json:"grid"`
|
||||
TradesExecuted int `json:"tradesExecuted"`
|
||||
TotalProfit float64 `json:"totalProfit"`
|
||||
LastUpdate *string `json:"lastUpdate,omitempty"`
|
||||
}
|
||||
|
||||
func (s *Server) handleGridCurrent(w http.ResponseWriter, r *http.Request) {
|
||||
state, err := s.gridRepo.GetActive(r.Context())
|
||||
if err != nil {
|
||||
fmt.Printf("Error fetching grid state: %v\n", err)
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if state == nil {
|
||||
writeJSON(w, http.StatusOK, gridCurrentResponse{
|
||||
Grid: json.RawMessage("[]"),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
grid := state.GridLevelsJSON
|
||||
if grid == nil {
|
||||
grid = json.RawMessage("[]")
|
||||
}
|
||||
|
||||
var lastUpdate *string
|
||||
ts := state.UpdatedAt.Format("2006-01-02T15:04:05.000Z")
|
||||
lastUpdate = &ts
|
||||
|
||||
writeJSON(w, http.StatusOK, gridCurrentResponse{
|
||||
BasePrice: state.BasePrice,
|
||||
Grid: grid,
|
||||
TradesExecuted: state.TradesExecuted,
|
||||
TotalProfit: state.TotalProfit,
|
||||
LastUpdate: lastUpdate,
|
||||
})
|
||||
}
|
||||
29
trahn-trade-backend/internal/api/health.go
Normal file
29
trahn-trade-backend/internal/api/health.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type healthResponse struct {
|
||||
Status string `json:"status"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
Services healthServices `json:"services"`
|
||||
}
|
||||
|
||||
type healthServices struct {
|
||||
Database string `json:"database"`
|
||||
}
|
||||
|
||||
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
dbStatus := "connected"
|
||||
if err := s.pool.Ping(r.Context()); err != nil {
|
||||
dbStatus = "disconnected"
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, healthResponse{
|
||||
Status: "ok",
|
||||
Timestamp: time.Now().UTC().Format(time.RFC3339),
|
||||
Services: healthServices{Database: dbStatus},
|
||||
})
|
||||
}
|
||||
187
trahn-trade-backend/internal/api/middleware_test.go
Normal file
187
trahn-trade-backend/internal/api/middleware_test.go
Normal file
@@ -0,0 +1,187 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAuthMiddleware_NoKeyConfigured(t *testing.T) {
|
||||
s := &Server{apiKey: ""}
|
||||
inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
handler := s.authMiddleware(inner)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/trades/stats", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200 when no API key configured, got %d", rr.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthMiddleware_HealthBypass(t *testing.T) {
|
||||
s := &Server{apiKey: "secret123"}
|
||||
inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
handler := s.authMiddleware(inner)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/health", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200 for /health without auth, got %d", rr.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthMiddleware_MissingHeader(t *testing.T) {
|
||||
s := &Server{apiKey: "secret123"}
|
||||
inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
handler := s.authMiddleware(inner)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/prices/latest", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("expected 401, got %d", rr.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthMiddleware_WrongKey(t *testing.T) {
|
||||
s := &Server{apiKey: "secret123"}
|
||||
inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
handler := s.authMiddleware(inner)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/prices/latest", nil)
|
||||
req.Header.Set("Authorization", "Bearer wrong_key")
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("expected 401, got %d", rr.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthMiddleware_CorrectKey(t *testing.T) {
|
||||
s := &Server{apiKey: "secret123"}
|
||||
inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
handler := s.authMiddleware(inner)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/prices/latest", nil)
|
||||
req.Header.Set("Authorization", "Bearer secret123")
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", rr.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthMiddleware_MalformedBearer(t *testing.T) {
|
||||
s := &Server{apiKey: "secret123"}
|
||||
inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
handler := s.authMiddleware(inner)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/prices/latest", nil)
|
||||
req.Header.Set("Authorization", "Basic secret123")
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("expected 401 for non-Bearer auth, got %d", rr.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDate(t *testing.T) {
|
||||
valid := []string{"2024-01-15", "2025-12-31", "2020-02-29"}
|
||||
for _, d := range valid {
|
||||
if !validateDate(d) {
|
||||
t.Fatalf("expected %q to be valid", d)
|
||||
}
|
||||
}
|
||||
|
||||
invalid := []string{
|
||||
"", "2024", "01-15-2024", "2024/01/15",
|
||||
"abcd-ef-gh", "2024-13-01", "2024-01-32",
|
||||
"2024-1-5", "20240115",
|
||||
}
|
||||
for _, d := range invalid {
|
||||
if validateDate(d) {
|
||||
t.Fatalf("expected %q to be invalid", d)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseLimit(t *testing.T) {
|
||||
cases := []struct {
|
||||
query string
|
||||
deflt int
|
||||
expected int
|
||||
}{
|
||||
{"", 100, 100},
|
||||
{"?limit=50", 100, 50},
|
||||
{"?limit=0", 100, 100},
|
||||
{"?limit=-5", 100, 100},
|
||||
{"?limit=abc", 100, 100},
|
||||
{"?limit=2000", 100, maxQueryLimit},
|
||||
{"?limit=1000", 100, 1000},
|
||||
{"?limit=1", 50, 1},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := httptest.NewRequest(http.MethodGet, "/test"+tc.query, nil)
|
||||
got := parseLimit(req, tc.deflt)
|
||||
if got != tc.expected {
|
||||
t.Fatalf("parseLimit(%q, %d) = %d, want %d", tc.query, tc.deflt, got, tc.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCorsMiddleware_Headers(t *testing.T) {
|
||||
inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
handler := corsMiddleware(inner, "https://myapp.example.com")
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/prices/latest", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
origin := rr.Header().Get("Access-Control-Allow-Origin")
|
||||
if origin != "https://myapp.example.com" {
|
||||
t.Fatalf("expected custom origin, got %q", origin)
|
||||
}
|
||||
|
||||
allow := rr.Header().Get("Access-Control-Allow-Headers")
|
||||
if allow == "" {
|
||||
t.Fatal("expected Allow-Headers to include Authorization")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCorsMiddleware_Preflight(t *testing.T) {
|
||||
inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatal("inner handler should not be called for OPTIONS")
|
||||
})
|
||||
handler := corsMiddleware(inner, "*")
|
||||
|
||||
req := httptest.NewRequest(http.MethodOptions, "/v1/prices/latest", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200 for preflight, got %d", rr.Code)
|
||||
}
|
||||
}
|
||||
79
trahn-trade-backend/internal/api/price_routes.go
Normal file
79
trahn-trade-backend/internal/api/price_routes.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/kjannette/trahn-backend/internal/repository"
|
||||
)
|
||||
|
||||
type priceJSON struct {
|
||||
T int64 `json:"t"`
|
||||
P float64 `json:"p"`
|
||||
}
|
||||
|
||||
func (s *Server) handlePricesToday(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
today := repository.TradingDayNow()
|
||||
prices, err := s.priceRepo.GetByDay(ctx, today)
|
||||
if err != nil {
|
||||
fmt.Printf("Error fetching today's prices: %v\n", err)
|
||||
writeError(w, http.StatusInternalServerError, "failed to fetch prices")
|
||||
return
|
||||
}
|
||||
|
||||
out := make([]priceJSON, len(prices))
|
||||
for i, p := range prices {
|
||||
out[i] = priceJSON{T: p.Timestamp.UnixMilli(), P: p.Price}
|
||||
}
|
||||
writeJSON(w, http.StatusOK, out)
|
||||
}
|
||||
|
||||
func (s *Server) handlePricesByDay(w http.ResponseWriter, r *http.Request) {
|
||||
date := r.PathValue("date")
|
||||
if !validateDate(date) {
|
||||
writeError(w, http.StatusBadRequest, "invalid date format, expected YYYY-MM-DD")
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
prices, err := s.priceRepo.GetByDay(ctx, date)
|
||||
if err != nil {
|
||||
fmt.Printf("Error fetching prices for %s: %v\n", date, err)
|
||||
writeError(w, http.StatusInternalServerError, "failed to fetch prices")
|
||||
return
|
||||
}
|
||||
|
||||
out := make([]priceJSON, len(prices))
|
||||
for i, p := range prices {
|
||||
out[i] = priceJSON{T: p.Timestamp.UnixMilli(), P: p.Price}
|
||||
}
|
||||
writeJSON(w, http.StatusOK, out)
|
||||
}
|
||||
|
||||
func (s *Server) handleAvailableDays(w http.ResponseWriter, r *http.Request) {
|
||||
days, err := s.priceRepo.GetAvailableDays(r.Context())
|
||||
if err != nil {
|
||||
fmt.Printf("Error fetching available days: %v\n", err)
|
||||
writeError(w, http.StatusInternalServerError, "failed to fetch available days")
|
||||
return
|
||||
}
|
||||
if days == nil {
|
||||
days = []string{}
|
||||
}
|
||||
writeJSON(w, http.StatusOK, days)
|
||||
}
|
||||
|
||||
func (s *Server) handleLatestPrice(w http.ResponseWriter, r *http.Request) {
|
||||
price, err := s.priceRepo.GetLatest(r.Context())
|
||||
if err != nil {
|
||||
fmt.Printf("Error fetching latest price: %v\n", err)
|
||||
writeError(w, http.StatusInternalServerError, "failed to fetch latest price")
|
||||
return
|
||||
}
|
||||
if price == nil {
|
||||
writeError(w, http.StatusNotFound, "no price data available")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, priceJSON{T: price.Timestamp.UnixMilli(), P: price.Price})
|
||||
}
|
||||
169
trahn-trade-backend/internal/api/server.go
Normal file
169
trahn-trade-backend/internal/api/server.go
Normal file
@@ -0,0 +1,169 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/kjannette/trahn-backend/internal/repository"
|
||||
)
|
||||
|
||||
const maxQueryLimit = 1000
|
||||
|
||||
var dateRegexp = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`)
|
||||
|
||||
type Server struct {
|
||||
pool *pgxpool.Pool
|
||||
priceRepo *repository.PriceRepo
|
||||
tradeRepo *repository.TradeRepo
|
||||
srRepo *repository.SRRepo
|
||||
gridRepo *repository.GridStateRepo
|
||||
httpServer *http.Server
|
||||
apiKey string
|
||||
}
|
||||
|
||||
func NewServer(pool *pgxpool.Pool, port int, apiKey, corsOrigin string) *Server {
|
||||
s := &Server{
|
||||
pool: pool,
|
||||
priceRepo: repository.NewPriceRepo(pool),
|
||||
tradeRepo: repository.NewTradeRepo(pool),
|
||||
srRepo: repository.NewSRRepo(pool),
|
||||
gridRepo: repository.NewGridStateRepo(pool),
|
||||
apiKey: apiKey,
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// Price routes
|
||||
mux.HandleFunc("GET /v1/prices/today", s.handlePricesToday)
|
||||
mux.HandleFunc("GET /v1/prices/day/{date}", s.handlePricesByDay)
|
||||
mux.HandleFunc("GET /v1/prices/days", s.handleAvailableDays)
|
||||
mux.HandleFunc("GET /v1/prices/latest", s.handleLatestPrice)
|
||||
|
||||
// Trade routes
|
||||
mux.HandleFunc("GET /v1/trades/today", s.handleTradesToday)
|
||||
mux.HandleFunc("GET /v1/trades/day/{date}", s.handleTradesByDay)
|
||||
mux.HandleFunc("GET /v1/trades/all", s.handleAllTrades)
|
||||
mux.HandleFunc("GET /v1/trades/stats", s.handleTradeStats)
|
||||
|
||||
// Grid routes
|
||||
mux.HandleFunc("GET /v1/grid/current", s.handleGridCurrent)
|
||||
|
||||
// S/R routes
|
||||
mux.HandleFunc("GET /v1/support-resistance/latest", s.handleSRLatest)
|
||||
mux.HandleFunc("GET /v1/support-resistance/history", s.handleSRHistory)
|
||||
|
||||
// Health check (no auth required)
|
||||
mux.HandleFunc("GET /health", s.handleHealth)
|
||||
|
||||
handler := s.authMiddleware(corsMiddleware(mux, corsOrigin))
|
||||
|
||||
s.httpServer = &http.Server{
|
||||
Addr: fmt.Sprintf(":%d", port),
|
||||
Handler: handler,
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Server) Start() error {
|
||||
fmt.Printf("[API] REST API server started on http://localhost%s\n", s.httpServer.Addr)
|
||||
fmt.Printf("[API] Health check: http://localhost%s/health\n", s.httpServer.Addr)
|
||||
if s.apiKey != "" {
|
||||
fmt.Println("[API] Authentication: enabled (Bearer token)")
|
||||
} else {
|
||||
fmt.Println("[API] Authentication: disabled (no API_KEY configured)")
|
||||
}
|
||||
return s.httpServer.ListenAndServe()
|
||||
}
|
||||
|
||||
func (s *Server) Shutdown(ctx context.Context) error {
|
||||
return s.httpServer.Shutdown(ctx)
|
||||
}
|
||||
|
||||
// --- middleware ---
|
||||
|
||||
func (s *Server) authMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if s.apiKey == "" || r.URL.Path == "/health" {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
auth := r.Header.Get("Authorization")
|
||||
if auth == "" {
|
||||
writeError(w, http.StatusUnauthorized, "missing Authorization header")
|
||||
return
|
||||
}
|
||||
|
||||
token := strings.TrimPrefix(auth, "Bearer ")
|
||||
if token == auth || token != s.apiKey {
|
||||
writeError(w, http.StatusUnauthorized, "invalid API key")
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func corsMiddleware(next http.Handler, allowOrigin string) http.Handler {
|
||||
if allowOrigin == "" {
|
||||
allowOrigin = "*"
|
||||
}
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", allowOrigin)
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// --- validation helpers ---
|
||||
|
||||
func validateDate(date string) bool {
|
||||
if !dateRegexp.MatchString(date) {
|
||||
return false
|
||||
}
|
||||
_, err := time.Parse("2006-01-02", date)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func parseLimit(r *http.Request, defaultLimit int) int {
|
||||
v := r.URL.Query().Get("limit")
|
||||
if v == "" {
|
||||
return defaultLimit
|
||||
}
|
||||
n, err := strconv.Atoi(v)
|
||||
if err != nil || n <= 0 {
|
||||
return defaultLimit
|
||||
}
|
||||
if n > maxQueryLimit {
|
||||
return maxQueryLimit
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// --- response helpers ---
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, v any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
|
||||
func writeError(w http.ResponseWriter, status int, msg string) {
|
||||
writeJSON(w, status, map[string]string{"error": msg})
|
||||
}
|
||||
52
trahn-trade-backend/internal/api/sr_routes.go
Normal file
52
trahn-trade-backend/internal/api/sr_routes.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type srLatestResponse struct {
|
||||
Support float64 `json:"support"`
|
||||
Resistance float64 `json:"resistance"`
|
||||
Midpoint float64 `json:"midpoint"`
|
||||
AvgPrice *float64 `json:"avgPrice"`
|
||||
Method string `json:"method"`
|
||||
LookbackDays int `json:"lookbackDays"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
}
|
||||
|
||||
func (s *Server) handleSRLatest(w http.ResponseWriter, r *http.Request) {
|
||||
sr, err := s.srRepo.GetLatest(r.Context())
|
||||
if err != nil {
|
||||
fmt.Printf("Error fetching latest S/R: %v\n", err)
|
||||
writeError(w, http.StatusInternalServerError, "failed to fetch S/R data")
|
||||
return
|
||||
}
|
||||
if sr == nil {
|
||||
writeError(w, http.StatusNotFound, "no S/R data available")
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, srLatestResponse{
|
||||
Support: sr.Support,
|
||||
Resistance: sr.Resistance,
|
||||
Midpoint: sr.Midpoint,
|
||||
AvgPrice: sr.AvgPrice,
|
||||
Method: sr.Method,
|
||||
LookbackDays: sr.LookbackDays,
|
||||
Timestamp: sr.Timestamp,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleSRHistory(w http.ResponseWriter, r *http.Request) {
|
||||
limit := parseLimit(r, 100)
|
||||
|
||||
history, err := s.srRepo.GetHistory(r.Context(), limit)
|
||||
if err != nil {
|
||||
fmt.Printf("Error fetching S/R history: %v\n", err)
|
||||
writeError(w, http.StatusInternalServerError, "failed to fetch S/R history")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, history)
|
||||
}
|
||||
132
trahn-trade-backend/internal/api/trade_routes.go
Normal file
132
trahn-trade-backend/internal/api/trade_routes.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/kjannette/trahn-backend/internal/repository"
|
||||
)
|
||||
|
||||
type tradeJSON struct {
|
||||
T int64 `json:"t"`
|
||||
Side string `json:"side"`
|
||||
Price float64 `json:"price"`
|
||||
Qty float64 `json:"qty"`
|
||||
GridLevel *int `json:"gridLevel,omitempty"`
|
||||
USDValue float64 `json:"usdValue"`
|
||||
IsPaperTrade bool `json:"isPaperTrade"`
|
||||
}
|
||||
|
||||
// parseTradeMode extracts the ?mode= query parameter.
|
||||
// Returns a *bool: nil = all, true = paper, false = live.
|
||||
func parseTradeMode(r *http.Request) (*bool, error) {
|
||||
v := r.URL.Query().Get("mode")
|
||||
switch v {
|
||||
case "", "all":
|
||||
return nil, nil
|
||||
case "paper":
|
||||
b := true
|
||||
return &b, nil
|
||||
case "live":
|
||||
b := false
|
||||
return &b, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid mode %q, expected paper|live|all", v)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleTradesToday(w http.ResponseWriter, r *http.Request) {
|
||||
mode, err := parseTradeMode(r)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
today := repository.TradingDayNow()
|
||||
|
||||
trades, err := s.tradeRepo.GetByDay(ctx, today, mode)
|
||||
if err != nil {
|
||||
fmt.Printf("Error fetching today's trades: %v\n", err)
|
||||
writeError(w, http.StatusInternalServerError, "failed to fetch trades")
|
||||
return
|
||||
}
|
||||
|
||||
out := make([]tradeJSON, len(trades))
|
||||
for i, t := range trades {
|
||||
out[i] = tradeJSON{
|
||||
T: t.Timestamp.UnixMilli(), Side: t.Side,
|
||||
Price: t.Price, Qty: t.Quantity,
|
||||
GridLevel: t.GridLevel, USDValue: t.USDValue,
|
||||
IsPaperTrade: t.IsPaperTrade,
|
||||
}
|
||||
}
|
||||
writeJSON(w, http.StatusOK, out)
|
||||
}
|
||||
|
||||
func (s *Server) handleTradesByDay(w http.ResponseWriter, r *http.Request) {
|
||||
date := r.PathValue("date")
|
||||
if !validateDate(date) {
|
||||
writeError(w, http.StatusBadRequest, "invalid date format, expected YYYY-MM-DD")
|
||||
return
|
||||
}
|
||||
|
||||
mode, err := parseTradeMode(r)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
trades, err := s.tradeRepo.GetByDay(ctx, date, mode)
|
||||
if err != nil {
|
||||
fmt.Printf("Error fetching trades for %s: %v\n", date, err)
|
||||
writeError(w, http.StatusInternalServerError, "failed to fetch trades")
|
||||
return
|
||||
}
|
||||
|
||||
out := make([]tradeJSON, len(trades))
|
||||
for i, t := range trades {
|
||||
out[i] = tradeJSON{
|
||||
T: t.Timestamp.UnixMilli(), Side: t.Side,
|
||||
Price: t.Price, Qty: t.Quantity,
|
||||
GridLevel: t.GridLevel, USDValue: t.USDValue,
|
||||
IsPaperTrade: t.IsPaperTrade,
|
||||
}
|
||||
}
|
||||
writeJSON(w, http.StatusOK, out)
|
||||
}
|
||||
|
||||
func (s *Server) handleAllTrades(w http.ResponseWriter, r *http.Request) {
|
||||
limit := parseLimit(r, 100)
|
||||
|
||||
mode, err := parseTradeMode(r)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
trades, err := s.tradeRepo.GetAll(r.Context(), limit, mode)
|
||||
if err != nil {
|
||||
fmt.Printf("Error fetching all trades: %v\n", err)
|
||||
writeError(w, http.StatusInternalServerError, "failed to fetch trades")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, trades)
|
||||
}
|
||||
|
||||
func (s *Server) handleTradeStats(w http.ResponseWriter, r *http.Request) {
|
||||
mode, err := parseTradeMode(r)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
stats, err := s.tradeRepo.GetStats(r.Context(), mode)
|
||||
if err != nil {
|
||||
fmt.Printf("Error fetching trade stats: %v\n", err)
|
||||
writeError(w, http.StatusInternalServerError, "failed to fetch trade stats")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, stats)
|
||||
}
|
||||
Reference in New Issue
Block a user