Files
zeekay f897cd1f9c feat: complete Alpaca Broker API — 100% endpoint coverage
Security fixes:
- Fix auth bypass: empty API key now returns 401 (unless BROKER_DEV_MODE=true)
- Fix risk engine recording $0 for all orders (now calculates qty*price)
- Fix hardcoded compliance fields in CreateAccount (pass-through from request)

P0 Trading Core:
- ReplaceOrder (PATCH), CancelAllOrders, GetPosition, ClosePosition, CloseAllPositions
- ListOrdersFiltered with status/limit/after/until/direction/nested params
- Extended CreateOrderRequest: trailing stops, bracket orders, extended hours

P1-P5 Complete API via composable interfaces:
- AccountManager: UpdateAccount, CloseAccount, GetAccountActivities
- DocumentManager: Upload, List, Get, Download documents
- JournalManager: Create, List, Get, Delete, Batch, ReverseBatch journals
- TransferExtended: CancelTransfer, DeleteACH, recipient bank CRUD
- CryptoDataProvider: GetCryptoBars/Quotes/Trades/Snapshots
- EventStreamer: SSE streaming for trade/account/transfer/journal events
- PortfolioAnalyzer: GetPortfolioHistory
- WatchlistManager: Full CRUD with asset management

Architecture: 8 optional capability interfaces on single Provider abstraction.
Providers implement what they support; router detects via type assertion.

Market data fixes:
- Crypto symbols now route to /v1beta3/crypto/us/ (not /v2/stocks/)
- GetBars pagination via next_page_token loop
2025-10-22 10:40:22 -07:00

129 lines
3.5 KiB
Go

package alpaca
import (
"context"
"encoding/json"
"net/http"
"github.com/luxfi/broker/pkg/types"
)
// --- WatchlistManager implementation ---
func (p *Provider) CreateWatchlist(ctx context.Context, accountID string, req *types.CreateWatchlistRequest) (*types.Watchlist, error) {
body := map[string]interface{}{
"name": req.Name,
}
if len(req.Symbols) > 0 {
body["symbols"] = req.Symbols
}
data, _, err := p.do(ctx, http.MethodPost, "/v1/trading/accounts/"+accountID+"/watchlists", body)
if err != nil {
return nil, err
}
return p.parseWatchlist(data)
}
func (p *Provider) ListWatchlists(ctx context.Context, accountID string) ([]*types.Watchlist, error) {
data, _, err := p.do(ctx, http.MethodGet, "/v1/trading/accounts/"+accountID+"/watchlists", nil)
if err != nil {
return nil, err
}
var raw []json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
return nil, err
}
watchlists := make([]*types.Watchlist, 0, len(raw))
for _, r := range raw {
w, err := p.parseWatchlist(r)
if err != nil {
continue
}
watchlists = append(watchlists, w)
}
return watchlists, nil
}
func (p *Provider) GetWatchlist(ctx context.Context, accountID, watchlistID string) (*types.Watchlist, error) {
data, _, err := p.do(ctx, http.MethodGet, "/v1/trading/accounts/"+accountID+"/watchlists/"+watchlistID, nil)
if err != nil {
return nil, err
}
return p.parseWatchlist(data)
}
func (p *Provider) UpdateWatchlist(ctx context.Context, accountID, watchlistID string, req *types.UpdateWatchlistRequest) (*types.Watchlist, error) {
body := make(map[string]interface{})
if req.Name != "" {
body["name"] = req.Name
}
if len(req.Symbols) > 0 {
body["symbols"] = req.Symbols
}
data, _, err := p.do(ctx, http.MethodPut, "/v1/trading/accounts/"+accountID+"/watchlists/"+watchlistID, body)
if err != nil {
return nil, err
}
return p.parseWatchlist(data)
}
func (p *Provider) DeleteWatchlist(ctx context.Context, accountID, watchlistID string) error {
_, _, err := p.do(ctx, http.MethodDelete, "/v1/trading/accounts/"+accountID+"/watchlists/"+watchlistID, nil)
return err
}
func (p *Provider) AddWatchlistAsset(ctx context.Context, accountID, watchlistID, symbol string) (*types.Watchlist, error) {
body := map[string]interface{}{
"symbol": symbol,
}
data, _, err := p.do(ctx, http.MethodPost, "/v1/trading/accounts/"+accountID+"/watchlists/"+watchlistID, body)
if err != nil {
return nil, err
}
return p.parseWatchlist(data)
}
func (p *Provider) RemoveWatchlistAsset(ctx context.Context, accountID, watchlistID, symbol string) error {
_, _, err := p.do(ctx, http.MethodDelete, "/v1/trading/accounts/"+accountID+"/watchlists/"+watchlistID+"/"+symbol, nil)
return err
}
func (p *Provider) parseWatchlist(data []byte) (*types.Watchlist, error) {
var raw struct {
ID string `json:"id"`
AccountID string `json:"account_id"`
Name string `json:"name"`
Assets []struct {
ID string `json:"id"`
Symbol string `json:"symbol"`
Name string `json:"name"`
Class string `json:"class"`
} `json:"assets"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
if err := json.Unmarshal(data, &raw); err != nil {
return nil, err
}
w := &types.Watchlist{
ID: raw.ID,
AccountID: raw.AccountID,
Name: raw.Name,
CreatedAt: raw.CreatedAt,
UpdatedAt: raw.UpdatedAt,
}
for _, a := range raw.Assets {
w.Assets = append(w.Assets, types.WatchlistAsset{
ID: a.ID,
Symbol: a.Symbol,
Name: a.Name,
Class: a.Class,
})
}
return w, nil
}