fix: gofmt -s across repo (CI format check)

This commit is contained in:
2026-06-02 11:37:59 -07:00
parent 25ae9559e5
commit d95bbdfc03
50 changed files with 1337 additions and 821 deletions
+3 -3
View File
@@ -26,9 +26,9 @@ type mappingKey struct {
// Resolver is a thread-safe store of user-to-provider account mappings.
type Resolver struct {
mu sync.RWMutex
byKey map[mappingKey]AccountMapping // user+provider -> mapping
byUser map[string][]AccountMapping // user -> all mappings
mu sync.RWMutex
byKey map[mappingKey]AccountMapping // user+provider -> mapping
byUser map[string][]AccountMapping // user -> all mappings
}
// NewResolver creates an empty Resolver.
+2 -2
View File
@@ -353,8 +353,8 @@ func TestAutoDiscoverNoMatchingAccounts(t *testing.T) {
func TestAutoDiscoverProviderError(t *testing.T) {
mp := &mockProviderForResolver{
name: "broken",
listErr: fmt.Errorf("connection refused"),
name: "broken",
listErr: fmt.Errorf("connection refused"),
}
registry := provider.NewRegistry()
+5 -5
View File
@@ -42,7 +42,7 @@ func RoleFromContext(ctx context.Context) string {
// Admin represents an admin user with hashed credentials.
type Admin struct {
Username string `json:"username"`
PasswordHash string `json:"-"` // bcrypt hash
PasswordHash string `json:"-"` // bcrypt hash
Role string `json:"role"` // super_admin, admin, reviewer
CreatedAt time.Time `json:"created_at"`
}
@@ -141,10 +141,10 @@ func (s *Store) ValidateToken(tokenStr string) (*Claims, error) {
// Claims are the JWT payload fields.
type Claims struct {
Sub string `json:"sub"` // username
Role string `json:"role"` // admin role
Iat int64 `json:"iat"` // issued at
Exp int64 `json:"exp"` // expires at
Sub string `json:"sub"` // username
Role string `json:"role"` // admin role
Iat int64 `json:"iat"` // issued at
Exp int64 `json:"exp"` // expires at
}
// Middleware returns HTTP middleware that validates admin JWT tokens.
+15 -15
View File
@@ -14,12 +14,12 @@ import (
// mockProvider implements provider.Provider for testing frontend handlers.
type mockProvider struct {
name string
accounts []*types.Account
assets []*types.Asset
snapshot map[string]*types.MarketSnapshot
bars []*types.Bar
orders []*types.Order
name string
accounts []*types.Account
assets []*types.Asset
snapshot map[string]*types.MarketSnapshot
bars []*types.Bar
orders []*types.Order
portfolio *types.Portfolio
}
@@ -253,9 +253,9 @@ func TestCryptoPricesWithData(t *testing.T) {
name: "testprov",
snapshot: map[string]*types.MarketSnapshot{
"BTC/USD": {
Symbol: "BTC/USD",
LatestTrade: &types.Trade{Price: 50000.0},
DailyBar: &types.Bar{Open: 49000, High: 51000, Low: 48000, Close: 50000, Volume: 1234},
Symbol: "BTC/USD",
LatestTrade: &types.Trade{Price: 50000.0},
DailyBar: &types.Bar{Open: 49000, High: 51000, Low: 48000, Close: 50000, Volume: 1234},
PrevDailyBar: &types.Bar{Close: 49000},
},
},
@@ -668,15 +668,15 @@ func TestCryptoPricesMultipleSymbols(t *testing.T) {
name: "testprov",
snapshot: map[string]*types.MarketSnapshot{
"BTC/USD": {
Symbol: "BTC/USD",
LatestTrade: &types.Trade{Price: 50000.0},
DailyBar: &types.Bar{Open: 49000, High: 51000, Low: 48000, Close: 50000, Volume: 1234},
Symbol: "BTC/USD",
LatestTrade: &types.Trade{Price: 50000.0},
DailyBar: &types.Bar{Open: 49000, High: 51000, Low: 48000, Close: 50000, Volume: 1234},
PrevDailyBar: &types.Bar{Close: 49000},
},
"ETH/USD": {
Symbol: "ETH/USD",
LatestTrade: &types.Trade{Price: 3000.0},
DailyBar: &types.Bar{Open: 2900, High: 3100, Low: 2800, Close: 3000, Volume: 5678},
Symbol: "ETH/USD",
LatestTrade: &types.Trade{Price: 3000.0},
DailyBar: &types.Bar{Open: 2900, High: 3100, Low: 2800, Close: 3000, Volume: 5678},
PrevDailyBar: &types.Bar{Close: 2900},
},
"SOL/USD": {
+2 -2
View File
@@ -40,8 +40,8 @@ const maxRequestBodyBytes = 64 * 1024
// validOptionActions is the allowlist for option order actions.
var validOptionActions = map[string]bool{
"buy_to_open": true,
"buy_to_close": true,
"buy_to_open": true,
"buy_to_close": true,
"sell_to_open": true,
"sell_to_close": true,
}
+5 -5
View File
@@ -247,10 +247,10 @@ func TestValidateOCCSymbolFormat(t *testing.T) {
{"", false},
{"AAPL", false},
{"not-valid", false},
{"AAPL260418X00150000", false}, // X is not C or P
{"aapl260418C00150000", false}, // lowercase
{"AAPL260418X00150000", false}, // X is not C or P
{"aapl260418C00150000", false}, // lowercase
{"TOOLONG260418C00150000", false}, // > 6 char root (7 chars)
{"AAPL26041800150000", false}, // missing C/P
{"AAPL26041800150000", false}, // missing C/P
{"<script>alert(1)</script>", false},
}
for _, tt := range tests {
@@ -270,9 +270,9 @@ func TestValidateUnderlyingSymbol(t *testing.T) {
{"SPY", true},
{"X", true},
{"TSLA", true},
{"aapl", true}, // case-insensitive (uppercased internally)
{"aapl", true}, // case-insensitive (uppercased internally)
{"", false},
{"TOOLONG1", false}, // > 6 chars
{"TOOLONG1", false}, // > 6 chars
{"123", false},
{"AAP-L", false},
{"AA PL", false},
-1
View File
@@ -123,4 +123,3 @@ func (s *Server) arbDetectorThreshold() float64 {
}
return 5 // default
}
+153 -153
View File
@@ -87,192 +87,192 @@ func NewServer(registry *provider.Registry, listenAddr string) *Server {
r.Use(auth.Middleware(iamEndpoint))
r.Route("/v1", func(r chi.Router) {
r.Get("/providers", s.handleListProviders)
r.Get("/providers/capabilities", s.handleGetCapabilities)
r.Get("/providers", s.handleListProviders)
r.Get("/providers/capabilities", s.handleGetCapabilities)
r.Get("/accounts", s.handleListAccounts)
r.Post("/accounts", s.handleCreateAccount)
r.Get("/accounts", s.handleListAccounts)
r.Post("/accounts", s.handleCreateAccount)
// Account-scoped routes require ownership verification.
r.Route("/accounts/{provider}/{accountId}", func(r chi.Router) {
r.Use(s.requireAccountOwnership)
// Account-scoped routes require ownership verification.
r.Route("/accounts/{provider}/{accountId}", func(r chi.Router) {
r.Use(s.requireAccountOwnership)
r.Get("/", s.handleGetAccount)
r.Get("/portfolio", s.handleGetPortfolio)
r.Get("/", s.handleGetAccount)
r.Get("/portfolio", s.handleGetPortfolio)
r.Get("/orders", s.handleListOrders)
r.Post("/orders", s.handleCreateOrder)
r.Delete("/orders", s.handleCancelAllOrders)
r.Get("/orders/{orderId}", s.handleGetOrder)
r.Patch("/orders/{orderId}", s.handleReplaceOrder)
r.Delete("/orders/{orderId}", s.handleCancelOrder)
r.Get("/orders", s.handleListOrders)
r.Post("/orders", s.handleCreateOrder)
r.Delete("/orders", s.handleCancelAllOrders)
r.Get("/orders/{orderId}", s.handleGetOrder)
r.Patch("/orders/{orderId}", s.handleReplaceOrder)
r.Delete("/orders/{orderId}", s.handleCancelOrder)
r.Get("/positions/{symbol}", s.handleGetPosition)
r.Delete("/positions/{symbol}", s.handleClosePosition)
r.Delete("/positions", s.handleCloseAllPositions)
r.Get("/positions/{symbol}", s.handleGetPosition)
r.Delete("/positions/{symbol}", s.handleClosePosition)
r.Delete("/positions", s.handleCloseAllPositions)
r.Get("/transfers", s.handleListTransfers)
r.Post("/transfers", s.handleCreateTransfer)
r.Get("/transfers", s.handleListTransfers)
r.Post("/transfers", s.handleCreateTransfer)
r.Get("/bank-relationships", s.handleListBankRelationships)
r.Post("/bank-relationships", s.handleCreateBankRelationship)
r.Get("/bank-relationships", s.handleListBankRelationships)
r.Post("/bank-relationships", s.handleCreateBankRelationship)
// Extended Account Management
r.Patch("/", s.handleUpdateAccount)
r.Delete("/", s.handleCloseAccount)
r.Get("/activities", s.handleGetAccountActivities)
// Extended Account Management
r.Patch("/", s.handleUpdateAccount)
r.Delete("/", s.handleCloseAccount)
r.Get("/activities", s.handleGetAccountActivities)
// Documents
r.Post("/documents", s.handleUploadDocument)
r.Get("/documents", s.handleListDocuments)
r.Get("/documents/{documentId}", s.handleGetDocument)
r.Get("/documents/{documentId}/download", s.handleDownloadDocument)
// Documents
r.Post("/documents", s.handleUploadDocument)
r.Get("/documents", s.handleListDocuments)
r.Get("/documents/{documentId}", s.handleGetDocument)
r.Get("/documents/{documentId}/download", s.handleDownloadDocument)
// Transfer Extended (cancel, ACH delete, wire banks)
r.Delete("/transfers/{transferId}", s.handleCancelTransfer)
r.Delete("/ach-relationships/{achId}", s.handleDeleteACHRelationship)
r.Post("/recipient-banks", s.handleCreateRecipientBank)
r.Get("/recipient-banks", s.handleListRecipientBanks)
r.Delete("/recipient-banks/{bankId}", s.handleDeleteRecipientBank)
// Transfer Extended (cancel, ACH delete, wire banks)
r.Delete("/transfers/{transferId}", s.handleCancelTransfer)
r.Delete("/ach-relationships/{achId}", s.handleDeleteACHRelationship)
r.Post("/recipient-banks", s.handleCreateRecipientBank)
r.Get("/recipient-banks", s.handleListRecipientBanks)
r.Delete("/recipient-banks/{bankId}", s.handleDeleteRecipientBank)
// ACATS Transfers
r.Post("/acats", s.handleCreateACATSTransfer)
r.Get("/acats", s.handleListACATSTransfers)
r.Get("/acats/{transferId}", s.handleGetACATSTransfer)
r.Delete("/acats/{transferId}", s.handleCancelACATSTransfer)
// ACATS Transfers
r.Post("/acats", s.handleCreateACATSTransfer)
r.Get("/acats", s.handleListACATSTransfers)
r.Get("/acats/{transferId}", s.handleGetACATSTransfer)
r.Delete("/acats/{transferId}", s.handleCancelACATSTransfer)
// Fixed Income
r.Post("/fixed-income/orders", s.handleCreateFixedIncomeOrder)
r.Get("/fixed-income/orders/{orderId}", s.handleGetFixedIncomeOrder)
r.Delete("/fixed-income/orders/{orderId}", s.handleCancelFixedIncomeOrder)
r.Get("/fixed-income/positions", s.handleGetFixedIncomePositions)
r.Delete("/fixed-income/positions/{symbol}", s.handleCloseFixedIncomePosition)
// Fixed Income
r.Post("/fixed-income/orders", s.handleCreateFixedIncomeOrder)
r.Get("/fixed-income/orders/{orderId}", s.handleGetFixedIncomeOrder)
r.Delete("/fixed-income/orders/{orderId}", s.handleCancelFixedIncomeOrder)
r.Get("/fixed-income/positions", s.handleGetFixedIncomePositions)
r.Delete("/fixed-income/positions/{symbol}", s.handleCloseFixedIncomePosition)
// Portfolio History
r.Get("/portfolio/history", s.handleGetPortfolioHistory)
// Portfolio History
r.Get("/portfolio/history", s.handleGetPortfolioHistory)
// Watchlists
r.Post("/watchlists", s.handleCreateWatchlist)
r.Get("/watchlists", s.handleListWatchlists)
r.Get("/watchlists/{watchlistId}", s.handleGetWatchlist)
r.Put("/watchlists/{watchlistId}", s.handleUpdateWatchlist)
r.Delete("/watchlists/{watchlistId}", s.handleDeleteWatchlist)
r.Post("/watchlists/{watchlistId}/assets", s.handleAddWatchlistAsset)
r.Delete("/watchlists/{watchlistId}/{symbol}", s.handleRemoveWatchlistAsset)
// Watchlists
r.Post("/watchlists", s.handleCreateWatchlist)
r.Get("/watchlists", s.handleListWatchlists)
r.Get("/watchlists/{watchlistId}", s.handleGetWatchlist)
r.Put("/watchlists/{watchlistId}", s.handleUpdateWatchlist)
r.Delete("/watchlists/{watchlistId}", s.handleDeleteWatchlist)
r.Post("/watchlists/{watchlistId}/assets", s.handleAddWatchlistAsset)
r.Delete("/watchlists/{watchlistId}/{symbol}", s.handleRemoveWatchlistAsset)
// Options Trading (account-scoped)
r.Post("/options/orders", s.handleCreateOptionOrder)
r.Post("/options/multi-leg", s.handleCreateMultiLegOrder)
r.Post("/options/exercise", s.handleExerciseOption)
r.Post("/options/do-not-exercise", s.handleDoNotExercise)
r.Get("/options/positions", s.handleGetOptionPositions)
}) // end /accounts/{provider}/{accountId}
// Options Trading (account-scoped)
r.Post("/options/orders", s.handleCreateOptionOrder)
r.Post("/options/multi-leg", s.handleCreateMultiLegOrder)
r.Post("/options/exercise", s.handleExerciseOption)
r.Post("/options/do-not-exercise", s.handleDoNotExercise)
r.Get("/options/positions", s.handleGetOptionPositions)
}) // end /accounts/{provider}/{accountId}
// ACATS disclosure is provider-scoped, not account-scoped.
r.Get("/accounts/{provider}/acats/disclosure", s.handleGetACATSDisclosure)
// ACATS disclosure is provider-scoped, not account-scoped.
r.Get("/accounts/{provider}/acats/disclosure", s.handleGetACATSDisclosure)
r.Get("/assets/{provider}", s.handleListAssets)
r.Get("/assets/{provider}/{symbolOrId}", s.handleGetAsset)
r.Get("/assets/{provider}", s.handleListAssets)
r.Get("/assets/{provider}/{symbolOrId}", s.handleGetAsset)
// Smart Order Routing
r.Get("/route/{symbol}", s.handleGetRoutes)
r.Get("/route/{symbol}/{quote}", s.handleGetRoutesPair)
r.Get("/route/{symbol}/split", s.handleGetSplitPlan)
r.Get("/route/{symbol}/{quote}/split", s.handleGetSplitPlanPair)
r.Get("/assets", s.handleAggregatedAssets)
r.Post("/smart-order", s.handleSmartOrder)
r.Post("/smart-order/split", s.handleExecuteSplit)
// Smart Order Routing
r.Get("/route/{symbol}", s.handleGetRoutes)
r.Get("/route/{symbol}/{quote}", s.handleGetRoutesPair)
r.Get("/route/{symbol}/split", s.handleGetSplitPlan)
r.Get("/route/{symbol}/{quote}/split", s.handleGetSplitPlanPair)
r.Get("/assets", s.handleAggregatedAssets)
r.Post("/smart-order", s.handleSmartOrder)
r.Post("/smart-order/split", s.handleExecuteSplit)
// TWAP Execution
r.Post("/twap", s.handleStartTWAP)
r.Get("/twap", s.handleGetTWAP)
r.Delete("/twap", s.handleCancelTWAP)
// TWAP Execution
r.Post("/twap", s.handleStartTWAP)
r.Get("/twap", s.handleGetTWAP)
r.Delete("/twap", s.handleCancelTWAP)
// Arbitrage Detection
r.Get("/arbitrage", s.handleScanArbitrage)
// Arbitrage Detection
r.Get("/arbitrage", s.handleScanArbitrage)
// Market Data
r.Get("/market/{provider}/snapshot/{symbol}", s.handleGetSnapshot)
r.Get("/market/{provider}/snapshots", s.handleGetSnapshots)
r.Get("/market/{provider}/bars/{symbol}", s.handleGetBars)
r.Get("/market/{provider}/trades/latest", s.handleGetLatestTrades)
r.Get("/market/{provider}/quotes/latest", s.handleGetLatestQuotes)
r.Get("/market/{provider}/clock", s.handleGetClock)
r.Get("/market/{provider}/calendar", s.handleGetCalendar)
// Market Data
r.Get("/market/{provider}/snapshot/{symbol}", s.handleGetSnapshot)
r.Get("/market/{provider}/snapshots", s.handleGetSnapshots)
r.Get("/market/{provider}/bars/{symbol}", s.handleGetBars)
r.Get("/market/{provider}/trades/latest", s.handleGetLatestTrades)
r.Get("/market/{provider}/quotes/latest", s.handleGetLatestQuotes)
r.Get("/market/{provider}/clock", s.handleGetClock)
r.Get("/market/{provider}/calendar", s.handleGetCalendar)
// Consolidated Market Data
r.Get("/bbo/{symbol}", s.handleGetBBO)
r.Get("/bbo/{symbol}/{quote}", s.handleGetBBOPair)
r.Get("/stream", s.stream.HandleSSE)
// Consolidated Market Data
r.Get("/bbo/{symbol}", s.handleGetBBO)
r.Get("/bbo/{symbol}/{quote}", s.handleGetBBOPair)
r.Get("/stream", s.stream.HandleSSE)
// Risk & Audit
r.Get("/risk/check", s.handleRiskCheck)
r.Get("/audit", s.handleAuditQuery)
r.Get("/audit/stats", s.handleAuditStats)
r.Get("/audit/export", s.handleAuditExport)
// Risk & Audit
r.Get("/risk/check", s.handleRiskCheck)
r.Get("/audit", s.handleAuditQuery)
r.Get("/audit/stats", s.handleAuditStats)
r.Get("/audit/export", s.handleAuditExport)
// Funding (deposit/withdraw via payment processors)
// Note: /v1/fund/webhook/{processor} is registered before auth middleware.
r.Route("/fund", func(r chi.Router) {
r.Post("/deposit", s.handleDeposit)
r.Post("/withdraw", s.handleWithdraw)
r.Get("/processors", s.handleListProcessors)
})
// Funding (deposit/withdraw via payment processors)
// Note: /v1/fund/webhook/{processor} is registered before auth middleware.
r.Route("/fund", func(r chi.Router) {
r.Post("/deposit", s.handleDeposit)
r.Post("/withdraw", s.handleWithdraw)
r.Get("/processors", s.handleListProcessors)
})
// Journals (inter-account transfers)
r.Post("/journals/{provider}", s.handleCreateJournal)
r.Get("/journals/{provider}", s.handleListJournals)
r.Get("/journals/{provider}/{journalId}", s.handleGetJournal)
r.Delete("/journals/{provider}/{journalId}", s.handleDeleteJournal)
r.Post("/journals/{provider}/batch", s.handleCreateBatchJournal)
r.Post("/journals/{provider}/reverse_batch", s.handleReverseBatchJournal)
// Journals (inter-account transfers)
r.Post("/journals/{provider}", s.handleCreateJournal)
r.Get("/journals/{provider}", s.handleListJournals)
r.Get("/journals/{provider}/{journalId}", s.handleGetJournal)
r.Delete("/journals/{provider}/{journalId}", s.handleDeleteJournal)
r.Post("/journals/{provider}/batch", s.handleCreateBatchJournal)
r.Post("/journals/{provider}/reverse_batch", s.handleReverseBatchJournal)
// Fixed Income (non-account-scoped)
r.Get("/assets/fixed-income/corporates", s.handleListCorporateBonds)
r.Get("/assets/fixed-income/treasuries", s.handleListTreasuryBonds)
// Fixed Income (non-account-scoped)
r.Get("/assets/fixed-income/corporates", s.handleListCorporateBonds)
r.Get("/assets/fixed-income/treasuries", s.handleListTreasuryBonds)
// Crypto Market Data
r.Get("/market/{provider}/crypto/bars", s.handleGetCryptoBars)
r.Get("/market/{provider}/crypto/quotes", s.handleGetCryptoQuotes)
r.Get("/market/{provider}/crypto/trades", s.handleGetCryptoTrades)
r.Get("/market/{provider}/crypto/snapshots", s.handleGetCryptoSnapshots)
// Crypto Market Data
r.Get("/market/{provider}/crypto/bars", s.handleGetCryptoBars)
r.Get("/market/{provider}/crypto/quotes", s.handleGetCryptoQuotes)
r.Get("/market/{provider}/crypto/trades", s.handleGetCryptoTrades)
r.Get("/market/{provider}/crypto/snapshots", s.handleGetCryptoSnapshots)
// Event Streaming (SSE)
r.Get("/events/{provider}/trades", s.handleStreamTradeEvents)
r.Get("/events/{provider}/accounts", s.handleStreamAccountEvents)
r.Get("/events/{provider}/transfers", s.handleStreamTransferEvents)
r.Get("/events/{provider}/journals", s.handleStreamJournalEvents)
// Event Streaming (SSE)
r.Get("/events/{provider}/trades", s.handleStreamTradeEvents)
r.Get("/events/{provider}/accounts", s.handleStreamAccountEvents)
r.Get("/events/{provider}/transfers", s.handleStreamTransferEvents)
r.Get("/events/{provider}/journals", s.handleStreamJournalEvents)
// Options Trading (non-account-scoped)
r.Get("/options/{provider}/expirations/{symbol}", s.handleGetOptionExpirations)
r.Get("/options/{provider}/chain/{symbol}", s.handleGetOptionChain)
r.Get("/options/{provider}/chain/{symbol}/{expiration}", s.handleGetOptionChain)
r.Get("/options/{provider}/quote/{contractSymbol}", s.handleGetOptionQuote)
r.Get("/options/{provider}/greeks/{contractSymbol}", s.handleGetOptionGreeks)
r.Get("/options/route", s.handleRouteOptionOrder)
// Options Trading (non-account-scoped)
r.Get("/options/{provider}/expirations/{symbol}", s.handleGetOptionExpirations)
r.Get("/options/{provider}/chain/{symbol}", s.handleGetOptionChain)
r.Get("/options/{provider}/chain/{symbol}/{expiration}", s.handleGetOptionChain)
r.Get("/options/{provider}/quote/{contractSymbol}", s.handleGetOptionQuote)
r.Get("/options/{provider}/greeks/{contractSymbol}", s.handleGetOptionGreeks)
r.Get("/options/route", s.handleRouteOptionOrder)
// Exchange frontend API (provider-agnostic, user-resolved)
r.Route("/exchange", func(r chi.Router) {
r.Get("/assets", s.handleFrontendAssets)
r.Get("/crypto-prices", s.handleCryptoPrices)
r.Get("/charts/{symbol}", s.handleChartData)
r.Get("/orders", s.handleFrontendOrders)
r.Post("/orders", s.handleFrontendCreateOrder)
r.Get("/positions", s.handleFrontendPositions)
r.Get("/portfolio", s.handleFrontendPortfolio)
// Exchange frontend API (provider-agnostic, user-resolved)
r.Route("/exchange", func(r chi.Router) {
r.Get("/assets", s.handleFrontendAssets)
r.Get("/crypto-prices", s.handleCryptoPrices)
r.Get("/charts/{symbol}", s.handleChartData)
r.Get("/orders", s.handleFrontendOrders)
r.Post("/orders", s.handleFrontendCreateOrder)
r.Get("/positions", s.handleFrontendPositions)
r.Get("/portfolio", s.handleFrontendPortfolio)
// Options (provider-agnostic, user-resolved)
r.Route("/options", func(r chi.Router) {
r.Post("/orders", s.handleExchangeCreateOptionOrder)
r.Post("/multi-leg", s.handleExchangeCreateMultiLegOrder)
r.Get("/chain/{symbol}", s.handleExchangeOptionChain)
r.Get("/expirations/{symbol}", s.handleExchangeOptionExpirations)
r.Get("/positions", s.handleExchangeOptionPositions)
r.Post("/orders/{id}/cancel", s.handleExchangeCancelOptionOrder)
r.Post("/exercise/{id}", s.handleExchangeExerciseOption)
// Options (provider-agnostic, user-resolved)
r.Route("/options", func(r chi.Router) {
r.Post("/orders", s.handleExchangeCreateOptionOrder)
r.Post("/multi-leg", s.handleExchangeCreateMultiLegOrder)
r.Get("/chain/{symbol}", s.handleExchangeOptionChain)
r.Get("/expirations/{symbol}", s.handleExchangeOptionExpirations)
r.Get("/positions", s.handleExchangeOptionPositions)
r.Post("/orders/{id}/cancel", s.handleExchangeCancelOptionOrder)
r.Post("/exercise/{id}", s.handleExchangeExerciseOption)
})
})
})
})
}) // end auth group
s.router = r
+18 -18
View File
@@ -24,24 +24,24 @@ type Log struct {
// Entry is a single audit log record.
type Entry struct {
ID string `json:"id"`
Timestamp time.Time `json:"timestamp"`
Action Action `json:"action"`
Provider string `json:"provider"`
AccountID string `json:"account_id"`
Symbol string `json:"symbol,omitempty"`
Side string `json:"side,omitempty"`
Qty string `json:"qty,omitempty"`
Price string `json:"price,omitempty"`
OrderID string `json:"order_id,omitempty"`
Algorithm string `json:"algorithm,omitempty"`
Status string `json:"status"` // success, failure, pending
Latency time.Duration `json:"latency_ns"` // execution latency
Error string `json:"error,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
ClientIP string `json:"client_ip,omitempty"`
UserAgent string `json:"user_agent,omitempty"`
RequestID string `json:"request_id,omitempty"`
ID string `json:"id"`
Timestamp time.Time `json:"timestamp"`
Action Action `json:"action"`
Provider string `json:"provider"`
AccountID string `json:"account_id"`
Symbol string `json:"symbol,omitempty"`
Side string `json:"side,omitempty"`
Qty string `json:"qty,omitempty"`
Price string `json:"price,omitempty"`
OrderID string `json:"order_id,omitempty"`
Algorithm string `json:"algorithm,omitempty"`
Status string `json:"status"` // success, failure, pending
Latency time.Duration `json:"latency_ns"` // execution latency
Error string `json:"error,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
ClientIP string `json:"client_ip,omitempty"`
UserAgent string `json:"user_agent,omitempty"`
RequestID string `json:"request_id,omitempty"`
}
// Action categorizes audit events.
+2 -2
View File
@@ -205,10 +205,10 @@ type walletScreenRequest struct {
// walletScreenResponse is the result of a wallet address screen.
type walletScreenResponse struct {
Address string `json:"address"`
Risk string `json:"risk"` // low, medium, high, blocked
Risk string `json:"risk"` // low, medium, high, blocked
Sanctioned bool `json:"sanctioned"`
Scam bool `json:"scam"`
Source string `json:"source"` // ofac, scamsniffer, jube
Source string `json:"source"` // ofac, scamsniffer, jube
}
// handleWalletScreen checks a crypto wallet address against OFAC SDN and Jube.
+2 -2
View File
@@ -222,8 +222,8 @@ func (h *applicationHandler) handleStep2(w http.ResponseWriter, r *http.Request)
}
var req struct {
Provider string `json:"provider"` // onfido, berbix, idmerit
VerificationID string `json:"verification_id"` // external verification ID
Provider string `json:"provider"` // onfido, berbix, idmerit
VerificationID string `json:"verification_id"` // external verification ID
Status string `json:"status,omitempty"` // pending, verified, failed
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+4 -2
View File
@@ -182,7 +182,8 @@ func TestKYCVerifyInvalidBody(t *testing.T) {
// Send a raw invalid JSON body.
req := httptest.NewRequest("POST", "/kyc/verify", bytes.NewBufferString("{invalid"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-User-Id", "test-user-001"); req.Header.Set("X-Org-Id", "built-in")
req.Header.Set("X-User-Id", "test-user-001")
req.Header.Set("X-Org-Id", "built-in")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
@@ -313,7 +314,8 @@ func TestPipelineCreateInvalidBody(t *testing.T) {
r, _ := newTestRouter()
req := httptest.NewRequest("POST", "/pipelines/", bytes.NewBufferString("not json"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-User-Id", "test-user-001"); req.Header.Set("X-Org-Id", "built-in")
req.Header.Set("X-User-Id", "test-user-001")
req.Header.Set("X-Org-Id", "built-in")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
+4 -4
View File
@@ -13,10 +13,10 @@ import (
// allowedDocMIMETypes restricts document uploads to safe formats.
var allowedDocMIMETypes = map[string]bool{
"application/pdf": true,
"image/jpeg": true,
"image/png": true,
"image/tiff": true,
"application/pdf": true,
"image/jpeg": true,
"image/png": true,
"image/tiff": true,
}
// maxDocumentContentSize is the maximum decoded size for base64 document content (10 MB).
+35 -35
View File
@@ -17,14 +17,14 @@ type TradeWebhookPayload struct {
// WebhookEvent is the event metadata envelope.
type WebhookEvent struct {
EventID string `json:"event_id"`
EventType string `json:"event_type"`
Endpoint string `json:"endpoint"`
Timestamp time.Time `json:"timestamp"`
Version string `json:"version"`
TransactionType string `json:"transaction_type"`
BlockchainTxID string `json:"blockchain_transaction_id"`
Recipients []Recipient `json:"recipients"`
EventID string `json:"event_id"`
EventType string `json:"event_type"`
Endpoint string `json:"endpoint"`
Timestamp time.Time `json:"timestamp"`
Version string `json:"version"`
TransactionType string `json:"transaction_type"`
BlockchainTxID string `json:"blockchain_transaction_id"`
Recipients []Recipient `json:"recipients"`
}
// Recipient is a webhook delivery target (broker-dealer or transfer agent).
@@ -50,29 +50,29 @@ type Transaction struct {
// Security describes the asset being traded.
type Security struct {
AssetID string `json:"asset_id"`
AssetName string `json:"asset_name"`
AssetType string `json:"asset_type"`
SecurityClass string `json:"security_class"`
ShareClass string `json:"share_class"`
CUSIP *string `json:"cusip"`
ISIN *string `json:"isin"`
IssuerID string `json:"issuer_id"`
IssuerName string `json:"issuer_name"`
IssuerType string `json:"issuer_type"`
NumberOfShares int `json:"number_of_shares"`
PricePerShare float64 `json:"price_per_share"`
Currency string `json:"currency"`
GrossTradeAmount float64 `json:"gross_trade_amount"`
AccruedInterest float64 `json:"accrued_interest"`
Commissions Commissions `json:"commissions"`
NetTradeAmount float64 `json:"net_trade_amount"`
TradeExecutionDatetime time.Time `json:"trade_execution_datetime"`
PriceDeterminationMethod string `json:"price_determination_method"`
BidPrice float64 `json:"bid_price"`
AskPrice float64 `json:"ask_price"`
LastValuationPrice float64 `json:"last_valuation_price"`
LastValuationDate string `json:"last_valuation_date"`
AssetID string `json:"asset_id"`
AssetName string `json:"asset_name"`
AssetType string `json:"asset_type"`
SecurityClass string `json:"security_class"`
ShareClass string `json:"share_class"`
CUSIP *string `json:"cusip"`
ISIN *string `json:"isin"`
IssuerID string `json:"issuer_id"`
IssuerName string `json:"issuer_name"`
IssuerType string `json:"issuer_type"`
NumberOfShares int `json:"number_of_shares"`
PricePerShare float64 `json:"price_per_share"`
Currency string `json:"currency"`
GrossTradeAmount float64 `json:"gross_trade_amount"`
AccruedInterest float64 `json:"accrued_interest"`
Commissions Commissions `json:"commissions"`
NetTradeAmount float64 `json:"net_trade_amount"`
TradeExecutionDatetime time.Time `json:"trade_execution_datetime"`
PriceDeterminationMethod string `json:"price_determination_method"`
BidPrice float64 `json:"bid_price"`
AskPrice float64 `json:"ask_price"`
LastValuationPrice float64 `json:"last_valuation_price"`
LastValuationDate string `json:"last_valuation_date"`
}
// Commissions holds fee information for both sides of the trade.
@@ -131,10 +131,10 @@ type ComplianceRef struct {
// TransferAgent represents the transfer agent handling the security transfer.
type TransferAgent struct {
FirmName string `json:"firm_name"`
SECRegistered bool `json:"sec_registered"`
SECRegistrationNumber string `json:"sec_registration_number"`
Acknowledgment TAcknowledgment `json:"acknowledgment"`
FirmName string `json:"firm_name"`
SECRegistered bool `json:"sec_registered"`
SECRegistrationNumber string `json:"sec_registration_number"`
Acknowledgment TAcknowledgment `json:"acknowledgment"`
}
// TAcknowledgment is the transfer agent's acknowledgment of the transfer instruction.
+4 -4
View File
@@ -224,10 +224,10 @@ func TestDispatcherRetryOnFailure(t *testing.T) {
d := NewDispatcher()
payload := &TradeWebhookPayload{
WebhookEvent: WebhookEvent{EventID: "evt_retry"},
Transaction: Transaction{TransactionID: "txn_retry"},
Buyer: Party{InvestorID: "inv_retry_buyer"},
Seller: Party{InvestorID: "inv_retry_seller"},
WebhookEvent: WebhookEvent{EventID: "evt_retry"},
Transaction: Transaction{TransactionID: "txn_retry"},
Buyer: Party{InvestorID: "inv_retry_buyer"},
Seller: Party{InvestorID: "inv_retry_seller"},
TransferAgent: TransferAgent{FirmName: "Retry TA"},
}
+18 -17
View File
@@ -15,18 +15,18 @@ import (
type CustodianType string
const (
CustodianPrime CustodianType = "prime" // prime broker (e.g., Alpaca, IBKR)
CustodianCrypto CustodianType = "crypto" // crypto custodian (e.g., Fireblocks, BitGo)
CustodianSelf CustodianType = "self" // self-custody wallet
CustodianBank CustodianType = "bank" // fiat bank custody
CustodianPrime CustodianType = "prime" // prime broker (e.g., Alpaca, IBKR)
CustodianCrypto CustodianType = "crypto" // crypto custodian (e.g., Fireblocks, BitGo)
CustodianSelf CustodianType = "self" // self-custody wallet
CustodianBank CustodianType = "bank" // fiat bank custody
)
// Custodian represents a registered custody provider.
type Custodian struct {
ID string `json:"id"`
Name string `json:"name"` // e.g., "fireblocks", "bitgo", "alpaca"
Type CustodianType `json:"type"`
Status string `json:"status"` // active, suspended, decommissioned
ID string `json:"id"`
Name string `json:"name"` // e.g., "fireblocks", "bitgo", "alpaca"
Type CustodianType `json:"type"`
Status string `json:"status"` // active, suspended, decommissioned
Metadata map[string]string `json:"metadata,omitempty"`
}
@@ -35,7 +35,7 @@ type Holding struct {
ID string `json:"id"`
AccountID string `json:"account_id"`
CustodianID string `json:"custodian_id"`
Asset string `json:"asset"` // symbol or currency
Asset string `json:"asset"` // symbol or currency
Quantity float64 `json:"quantity"`
CostBasis float64 `json:"cost_basis"` // USD cost basis for tax reporting
UpdatedAt time.Time `json:"updated_at"`
@@ -67,11 +67,11 @@ type Transfer struct {
// ReconciliationResult is the outcome of comparing our records against a custodian.
type ReconciliationResult struct {
CustodianID string `json:"custodian_id"`
Timestamp time.Time `json:"timestamp"`
Matched int `json:"matched"`
Mismatched int `json:"mismatched"`
Breaks []ReconciliationBreak `json:"breaks,omitempty"`
CustodianID string `json:"custodian_id"`
Timestamp time.Time `json:"timestamp"`
Matched int `json:"matched"`
Mismatched int `json:"mismatched"`
Breaks []ReconciliationBreak `json:"breaks,omitempty"`
}
// ReconciliationBreak is a discrepancy between our records and the custodian.
@@ -86,14 +86,15 @@ type ReconciliationBreak struct {
// BalanceFetcher retrieves current balances from a custody provider.
// Implementations wrap provider-specific APIs (Fireblocks, BitGo, etc.).
type BalanceFetcher func(ctx context.Context, custodianID string) (map[string]map[string]float64, error)
// Returns: accountID -> asset -> quantity
// Service manages custody state, inter-custodian transfers, and reconciliation.
type Service struct {
mu sync.RWMutex
custodians map[string]*Custodian // custodianID -> custodian
holdings map[holdingKey]*Holding // (account, custodian, asset) -> holding
transfers map[string]*Transfer // transferID -> transfer
custodians map[string]*Custodian // custodianID -> custodian
holdings map[holdingKey]*Holding // (account, custodian, asset) -> holding
transfers map[string]*Transfer // transferID -> transfer
nextID int
}
+4 -4
View File
@@ -28,9 +28,9 @@ func NewWithRegistry(r *processor.Registry) *Service {
type DepositRequest struct {
AccountID string `json:"account_id"`
Provider string `json:"provider"`
Amount int64 `json:"amount"` // cents
Currency string `json:"currency"` // e.g. "usd", "btc"
PaymentMethod string `json:"payment_method"` // "card", "bank_transfer", "crypto"
Amount int64 `json:"amount"` // cents
Currency string `json:"currency"` // e.g. "usd", "btc"
PaymentMethod string `json:"payment_method"` // "card", "bank_transfer", "crypto"
Token string `json:"token,omitempty"` // payment method nonce/token (card)
TxHash string `json:"tx_hash,omitempty"` // on-chain tx hash (crypto)
Chain string `json:"chain,omitempty"` // blockchain network (crypto)
@@ -41,7 +41,7 @@ type DepositRequest struct {
type WithdrawRequest struct {
AccountID string `json:"account_id"`
Provider string `json:"provider"`
Amount int64 `json:"amount"` // cents
Amount int64 `json:"amount"` // cents
Currency string `json:"currency"`
PaymentMethod string `json:"payment_method"` // "bank_transfer", "crypto"
RelationshipID string `json:"relationship_id,omitempty"`
+10 -10
View File
@@ -7,16 +7,16 @@ import (
// ArbitrageOpportunity represents a detected cross-venue price discrepancy
// where the best bid on one venue exceeds the best ask on another.
type ArbitrageOpportunity struct {
Symbol string `json:"symbol"`
BuyVenue string `json:"buy_venue"` // venue with the lower ask
SellVenue string `json:"sell_venue"` // venue with the higher bid
BuyPrice float64 `json:"buy_price"` // ask on buy venue
SellPrice float64 `json:"sell_price"` // bid on sell venue
SpreadAbs float64 `json:"spread_abs"` // sell - buy
SpreadBps float64 `json:"spread_bps"` // in basis points
BuySize float64 `json:"buy_size"` // available size at ask
SellSize float64 `json:"sell_size"` // available size at bid
DetectedAt time.Time `json:"detected_at"`
Symbol string `json:"symbol"`
BuyVenue string `json:"buy_venue"` // venue with the lower ask
SellVenue string `json:"sell_venue"` // venue with the higher bid
BuyPrice float64 `json:"buy_price"` // ask on buy venue
SellPrice float64 `json:"sell_price"` // bid on sell venue
SpreadAbs float64 `json:"spread_abs"` // sell - buy
SpreadBps float64 `json:"spread_bps"` // in basis points
BuySize float64 `json:"buy_size"` // available size at ask
SellSize float64 `json:"sell_size"` // available size at bid
DetectedAt time.Time `json:"detected_at"`
}
// ArbitrageDetector scans the consolidated feed for cross-venue arbitrage.
+26 -26
View File
@@ -13,19 +13,19 @@ import (
// It maintains a consolidated order book and best bid/offer (BBO)
// for smart order routing decisions.
type Feed struct {
mu sync.RWMutex
books map[string]*ConsolidatedBook // symbol -> consolidated book
tickers map[string]*Ticker // symbol -> latest ticker
subs map[string][]chan *Ticker // symbol -> subscriber channels
subsMu sync.RWMutex
mu sync.RWMutex
books map[string]*ConsolidatedBook // symbol -> consolidated book
tickers map[string]*Ticker // symbol -> latest ticker
subs map[string][]chan *Ticker // symbol -> subscriber channels
subsMu sync.RWMutex
}
// ConsolidatedBook aggregates order book depth across providers.
type ConsolidatedBook struct {
Symbol string `json:"symbol"`
Bids []PriceLevel `json:"bids"` // sorted best (highest) first
Asks []PriceLevel `json:"asks"` // sorted best (lowest) first
UpdatedAt time.Time `json:"updated_at"`
Symbol string `json:"symbol"`
Bids []PriceLevel `json:"bids"` // sorted best (highest) first
Asks []PriceLevel `json:"asks"` // sorted best (lowest) first
UpdatedAt time.Time `json:"updated_at"`
}
// PriceLevel is a single price/quantity level with provider attribution.
@@ -37,27 +37,27 @@ type PriceLevel struct {
// Ticker is a consolidated real-time price.
type Ticker struct {
Symbol string `json:"symbol"`
BestBid float64 `json:"best_bid"`
BestAsk float64 `json:"best_ask"`
BestBidProvider string `json:"best_bid_provider"`
BestAskProvider string `json:"best_ask_provider"`
Spread float64 `json:"spread"`
SpreadBps float64 `json:"spread_bps"` // basis points
Last float64 `json:"last"`
Volume24h float64 `json:"volume_24h,omitempty"`
Sources map[string]*Quote `json:"sources"` // per-provider quotes
UpdatedAt time.Time `json:"updated_at"`
Symbol string `json:"symbol"`
BestBid float64 `json:"best_bid"`
BestAsk float64 `json:"best_ask"`
BestBidProvider string `json:"best_bid_provider"`
BestAskProvider string `json:"best_ask_provider"`
Spread float64 `json:"spread"`
SpreadBps float64 `json:"spread_bps"` // basis points
Last float64 `json:"last"`
Volume24h float64 `json:"volume_24h,omitempty"`
Sources map[string]*Quote `json:"sources"` // per-provider quotes
UpdatedAt time.Time `json:"updated_at"`
}
// Quote is a single provider's quote.
type Quote struct {
Provider string `json:"provider"`
BidPrice float64 `json:"bid_price"`
BidSize float64 `json:"bid_size"`
AskPrice float64 `json:"ask_price"`
AskSize float64 `json:"ask_size"`
Last float64 `json:"last"`
Provider string `json:"provider"`
BidPrice float64 `json:"bid_price"`
BidSize float64 `json:"bid_size"`
AskPrice float64 `json:"ask_price"`
AskSize float64 `json:"ask_size"`
Last float64 `json:"last"`
UpdatedAt time.Time `json:"updated_at"`
}
+63 -21
View File
@@ -38,24 +38,66 @@ func New(cfg Config) *Provider {
}
func (p *Provider) Name() string { return "adgm" }
func (p *Provider) CreateAccount(_ context.Context, _ *types.CreateAccountRequest) (*types.Account, error) { return nil, fmt.Errorf("adgm: CreateAccount not implemented") }
func (p *Provider) GetAccount(_ context.Context, _ string) (*types.Account, error) { return nil, fmt.Errorf("adgm: GetAccount not implemented") }
func (p *Provider) ListAccounts(_ context.Context) ([]*types.Account, error) { return nil, fmt.Errorf("adgm: ListAccounts not implemented") }
func (p *Provider) GetPortfolio(_ context.Context, _ string) (*types.Portfolio, error) { return nil, fmt.Errorf("adgm: GetPortfolio not implemented") }
func (p *Provider) CreateOrder(_ context.Context, _ string, _ *types.CreateOrderRequest) (*types.Order, error) { return nil, fmt.Errorf("adgm: CreateOrder not implemented") }
func (p *Provider) ListOrders(_ context.Context, _ string) ([]*types.Order, error) { return nil, fmt.Errorf("adgm: ListOrders not implemented") }
func (p *Provider) GetOrder(_ context.Context, _, _ string) (*types.Order, error) { return nil, fmt.Errorf("adgm: GetOrder not implemented") }
func (p *Provider) CancelOrder(_ context.Context, _, _ string) error { return fmt.Errorf("adgm: CancelOrder not implemented") }
func (p *Provider) CreateTransfer(_ context.Context, _ string, _ *types.CreateTransferRequest) (*types.Transfer, error) { return nil, fmt.Errorf("adgm: CreateTransfer not implemented") }
func (p *Provider) ListTransfers(_ context.Context, _ string) ([]*types.Transfer, error) { return nil, fmt.Errorf("adgm: ListTransfers not implemented") }
func (p *Provider) CreateBankRelationship(_ context.Context, _, _, _, _, _ string) (*types.BankRelationship, error) { return nil, fmt.Errorf("adgm: CreateBankRelationship not implemented") }
func (p *Provider) ListBankRelationships(_ context.Context, _ string) ([]*types.BankRelationship, error) { return nil, fmt.Errorf("adgm: ListBankRelationships not implemented") }
func (p *Provider) ListAssets(_ context.Context, _ string) ([]*types.Asset, error) { return nil, fmt.Errorf("adgm: ListAssets not implemented") }
func (p *Provider) GetAsset(_ context.Context, _ string) (*types.Asset, error) { return nil, fmt.Errorf("adgm: GetAsset not implemented") }
func (p *Provider) GetSnapshot(_ context.Context, _ string) (*types.MarketSnapshot, error) { return nil, fmt.Errorf("adgm: GetSnapshot not implemented") }
func (p *Provider) GetSnapshots(_ context.Context, _ []string) (map[string]*types.MarketSnapshot, error) { return nil, fmt.Errorf("adgm: GetSnapshots not implemented") }
func (p *Provider) GetBars(_ context.Context, _, _, _, _ string, _ int) ([]*types.Bar, error) { return nil, fmt.Errorf("adgm: GetBars not implemented") }
func (p *Provider) GetLatestTrades(_ context.Context, _ []string) (map[string]*types.Trade, error) { return nil, fmt.Errorf("adgm: GetLatestTrades not implemented") }
func (p *Provider) GetLatestQuotes(_ context.Context, _ []string) (map[string]*types.Quote, error) { return nil, fmt.Errorf("adgm: GetLatestQuotes not implemented") }
func (p *Provider) GetClock(_ context.Context) (*types.MarketClock, error) { return nil, fmt.Errorf("adgm: GetClock not implemented") }
func (p *Provider) GetCalendar(_ context.Context, _, _ string) ([]*types.MarketCalendarDay, error) { return nil, fmt.Errorf("adgm: GetCalendar not implemented") }
func (p *Provider) CreateAccount(_ context.Context, _ *types.CreateAccountRequest) (*types.Account, error) {
return nil, fmt.Errorf("adgm: CreateAccount not implemented")
}
func (p *Provider) GetAccount(_ context.Context, _ string) (*types.Account, error) {
return nil, fmt.Errorf("adgm: GetAccount not implemented")
}
func (p *Provider) ListAccounts(_ context.Context) ([]*types.Account, error) {
return nil, fmt.Errorf("adgm: ListAccounts not implemented")
}
func (p *Provider) GetPortfolio(_ context.Context, _ string) (*types.Portfolio, error) {
return nil, fmt.Errorf("adgm: GetPortfolio not implemented")
}
func (p *Provider) CreateOrder(_ context.Context, _ string, _ *types.CreateOrderRequest) (*types.Order, error) {
return nil, fmt.Errorf("adgm: CreateOrder not implemented")
}
func (p *Provider) ListOrders(_ context.Context, _ string) ([]*types.Order, error) {
return nil, fmt.Errorf("adgm: ListOrders not implemented")
}
func (p *Provider) GetOrder(_ context.Context, _, _ string) (*types.Order, error) {
return nil, fmt.Errorf("adgm: GetOrder not implemented")
}
func (p *Provider) CancelOrder(_ context.Context, _, _ string) error {
return fmt.Errorf("adgm: CancelOrder not implemented")
}
func (p *Provider) CreateTransfer(_ context.Context, _ string, _ *types.CreateTransferRequest) (*types.Transfer, error) {
return nil, fmt.Errorf("adgm: CreateTransfer not implemented")
}
func (p *Provider) ListTransfers(_ context.Context, _ string) ([]*types.Transfer, error) {
return nil, fmt.Errorf("adgm: ListTransfers not implemented")
}
func (p *Provider) CreateBankRelationship(_ context.Context, _, _, _, _, _ string) (*types.BankRelationship, error) {
return nil, fmt.Errorf("adgm: CreateBankRelationship not implemented")
}
func (p *Provider) ListBankRelationships(_ context.Context, _ string) ([]*types.BankRelationship, error) {
return nil, fmt.Errorf("adgm: ListBankRelationships not implemented")
}
func (p *Provider) ListAssets(_ context.Context, _ string) ([]*types.Asset, error) {
return nil, fmt.Errorf("adgm: ListAssets not implemented")
}
func (p *Provider) GetAsset(_ context.Context, _ string) (*types.Asset, error) {
return nil, fmt.Errorf("adgm: GetAsset not implemented")
}
func (p *Provider) GetSnapshot(_ context.Context, _ string) (*types.MarketSnapshot, error) {
return nil, fmt.Errorf("adgm: GetSnapshot not implemented")
}
func (p *Provider) GetSnapshots(_ context.Context, _ []string) (map[string]*types.MarketSnapshot, error) {
return nil, fmt.Errorf("adgm: GetSnapshots not implemented")
}
func (p *Provider) GetBars(_ context.Context, _, _, _, _ string, _ int) ([]*types.Bar, error) {
return nil, fmt.Errorf("adgm: GetBars not implemented")
}
func (p *Provider) GetLatestTrades(_ context.Context, _ []string) (map[string]*types.Trade, error) {
return nil, fmt.Errorf("adgm: GetLatestTrades not implemented")
}
func (p *Provider) GetLatestQuotes(_ context.Context, _ []string) (map[string]*types.Quote, error) {
return nil, fmt.Errorf("adgm: GetLatestQuotes not implemented")
}
func (p *Provider) GetClock(_ context.Context) (*types.MarketClock, error) {
return nil, fmt.Errorf("adgm: GetClock not implemented")
}
func (p *Provider) GetCalendar(_ context.Context, _, _ string) ([]*types.MarketCalendarDay, error) {
return nil, fmt.Errorf("adgm: GetCalendar not implemented")
}
+9 -9
View File
@@ -130,13 +130,13 @@ type alpacaAccount struct {
AccountType string `json:"account_type"`
CreatedAt string `json:"created_at"`
Contact *struct {
EmailAddress string `json:"email_address"`
PhoneNumber string `json:"phone_number"`
EmailAddress string `json:"email_address"`
PhoneNumber string `json:"phone_number"`
StreetAddress []string `json:"street_address"`
City string `json:"city"`
State string `json:"state"`
PostalCode string `json:"postal_code"`
Country string `json:"country"`
City string `json:"city"`
State string `json:"state"`
PostalCode string `json:"postal_code"`
Country string `json:"country"`
} `json:"contact"`
Identity *struct {
GivenName string `json:"given_name"`
@@ -229,7 +229,7 @@ func (p *Provider) CreateAccount(ctx context.Context, req *types.CreateAccountRe
"country": req.Contact.Country,
},
"identity": map[string]interface{}{
"given_name": req.Identity.GivenName,
"given_name": req.Identity.GivenName,
"family_name": req.Identity.FamilyName,
"date_of_birth": req.Identity.DateOfBirth,
"tax_id": req.Identity.TaxID,
@@ -401,8 +401,8 @@ func (p *Provider) CreateOrder(ctx context.Context, providerAccountID string, re
et, _ := time.LoadLocation("America/New_York")
now := time.Now().In(et)
hour, min := now.Hour(), now.Minute()
marketOpen := hour > 9 || (hour == 9 && min >= 30) // 9:30 AM
marketClose := hour < 16 // 4:00 PM
marketOpen := hour > 9 || (hour == 9 && min >= 30) // 9:30 AM
marketClose := hour < 16 // 4:00 PM
isMarketHours := marketOpen && marketClose && now.Weekday() >= time.Monday && now.Weekday() <= time.Friday
if !isMarketHours {
tif := strings.ToLower(req.TimeInForce)
+10 -10
View File
@@ -145,16 +145,16 @@ func TestCreateACATSTransfer_Full(t *testing.T) {
return
}
json.NewEncoder(w).Encode(map[string]interface{}{
"id": "xfer-1",
"account_id": "acct-1",
"direction": "INCOMING",
"status": "QUEUED",
"type": "ACATS",
"transfer_type": "FULL",
"id": "xfer-1",
"account_id": "acct-1",
"direction": "INCOMING",
"status": "QUEUED",
"type": "ACATS",
"transfer_type": "FULL",
"contra_account_number": "999-888",
"contra_broker_number": "0123",
"created_at": "2026-04-05T10:00:00Z",
"updated_at": "2026-04-05T10:00:00Z",
"created_at": "2026-04-05T10:00:00Z",
"updated_at": "2026-04-05T10:00:00Z",
})
},
})
@@ -207,7 +207,7 @@ func TestGetACATSTransfer(t *testing.T) {
"/v1/accounts/acct-1/transfers/xfer-1": func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]interface{}{
"id": "xfer-1",
"account_id": "acct-1",
"account_id": "acct-1",
"status": "APPROVED",
"direction": "INCOMING",
"type": "ACATS",
@@ -232,7 +232,7 @@ func TestGetACATSRejection(t *testing.T) {
"/v1/accounts/acct-1/transfers/xfer-rej": func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]interface{}{
"id": "xfer-rej",
"account_id": "acct-1",
"account_id": "acct-1",
"status": "REJECTED",
"direction": "INCOMING",
"type": "ACATS",
+9 -9
View File
@@ -27,15 +27,15 @@ func (p *Provider) StreamTradeEvents(ctx context.Context, since string) (<-chan
EventID string `json:"event_id"`
AccountID string `json:"account_id"`
Order *struct {
ID string `json:"id"`
Symbol string `json:"symbol"`
Side string `json:"side"`
Type string `json:"type"`
Qty string `json:"qty"`
FilledQty string `json:"filled_qty"`
FilledAvgPrice string `json:"filled_avg_price"`
Status string `json:"status"`
CreatedAt string `json:"created_at"`
ID string `json:"id"`
Symbol string `json:"symbol"`
Side string `json:"side"`
Type string `json:"type"`
Qty string `json:"qty"`
FilledQty string `json:"filled_qty"`
FilledAvgPrice string `json:"filled_avg_price"`
Status string `json:"status"`
CreatedAt string `json:"created_at"`
} `json:"order"`
Timestamp string `json:"timestamp"`
}
@@ -106,15 +106,15 @@ func TestCreateFixedIncomeOrder_Buy(t *testing.T) {
}
json.NewEncoder(w).Encode(map[string]interface{}{
"id": "order-fi-1",
"symbol": "037833100",
"side": "buy",
"type": "market",
"time_in_force": "day",
"status": "accepted",
"qty": "5000",
"asset_class": "us_equity",
"created_at": "2026-04-05T10:00:00Z",
"id": "order-fi-1",
"symbol": "037833100",
"side": "buy",
"type": "market",
"time_in_force": "day",
"status": "accepted",
"qty": "5000",
"asset_class": "us_equity",
"created_at": "2026-04-05T10:00:00Z",
})
},
})
+15 -15
View File
@@ -16,21 +16,21 @@ import (
// Alpaca options contract response from /v1/options/contracts
type alpacaOptionContract struct {
ID string `json:"id"`
Symbol string `json:"symbol"`
Name string `json:"name"`
Status string `json:"status"`
Tradable bool `json:"tradable"`
ExpirationDate string `json:"expiration_date"`
RootSymbol string `json:"root_symbol"`
UnderlyingSymbol string `json:"underlying_symbol"`
Type string `json:"type"` // call, put
Style string `json:"style"`
StrikePrice string `json:"strike_price"`
Size string `json:"size"`
OpenInterest string `json:"open_interest"`
ClosePrice string `json:"close_price"`
OpenPrice string `json:"open_price"`
ID string `json:"id"`
Symbol string `json:"symbol"`
Name string `json:"name"`
Status string `json:"status"`
Tradable bool `json:"tradable"`
ExpirationDate string `json:"expiration_date"`
RootSymbol string `json:"root_symbol"`
UnderlyingSymbol string `json:"underlying_symbol"`
Type string `json:"type"` // call, put
Style string `json:"style"`
StrikePrice string `json:"strike_price"`
Size string `json:"size"`
OpenInterest string `json:"open_interest"`
ClosePrice string `json:"close_price"`
OpenPrice string `json:"open_price"`
}
// Alpaca option snapshot from data API
+14 -14
View File
@@ -305,16 +305,16 @@ func TestCreateOptionOrder(t *testing.T) {
}
json.NewDecoder(r.Body).Decode(&gotBody)
json.NewEncoder(w).Encode(map[string]interface{}{
"id": "order-123",
"symbol": "AAPL260418C00150000",
"qty": "1",
"side": "buy",
"type": "limit",
"time_in_force": "day",
"limit_price": "5.50",
"status": "accepted",
"asset_class": "us_option",
"created_at": "2026-04-01T10:00:00Z",
"id": "order-123",
"symbol": "AAPL260418C00150000",
"qty": "1",
"side": "buy",
"type": "limit",
"time_in_force": "day",
"limit_price": "5.50",
"status": "accepted",
"asset_class": "us_option",
"created_at": "2026-04-01T10:00:00Z",
})
},
})
@@ -400,10 +400,10 @@ func TestCreateMultiLegOrder(t *testing.T) {
"/v1/trading/accounts/test-acct/orders": func(w http.ResponseWriter, r *http.Request) {
json.NewDecoder(r.Body).Decode(&gotBody)
json.NewEncoder(w).Encode(map[string]interface{}{
"id": "multileg-789",
"symbol": "AAPL",
"status": "accepted",
"created_at": "2026-04-01T10:00:00Z",
"id": "multileg-789",
"symbol": "AAPL",
"status": "accepted",
"created_at": "2026-04-01T10:00:00Z",
})
},
})
+8 -8
View File
@@ -6,13 +6,13 @@ import (
// Compile-time assertions that *Provider implements all optional interfaces.
var (
_ provider.Provider = (*Provider)(nil)
_ provider.AccountManager = (*Provider)(nil)
_ provider.DocumentManager = (*Provider)(nil)
_ provider.JournalManager = (*Provider)(nil)
_ provider.TransferExtended = (*Provider)(nil)
_ provider.Provider = (*Provider)(nil)
_ provider.AccountManager = (*Provider)(nil)
_ provider.DocumentManager = (*Provider)(nil)
_ provider.JournalManager = (*Provider)(nil)
_ provider.TransferExtended = (*Provider)(nil)
_ provider.CryptoDataProvider = (*Provider)(nil)
_ provider.EventStreamer = (*Provider)(nil)
_ provider.PortfolioAnalyzer = (*Provider)(nil)
_ provider.WatchlistManager = (*Provider)(nil)
_ provider.EventStreamer = (*Provider)(nil)
_ provider.PortfolioAnalyzer = (*Provider)(nil)
_ provider.WatchlistManager = (*Provider)(nil)
)
@@ -96,7 +96,7 @@ func (p *Provider) CreateAccount(ctx context.Context, req *types.CreateAccountRe
"country": req.Contact.Country,
},
"identity": map[string]interface{}{
"given_name": req.Identity.GivenName,
"given_name": req.Identity.GivenName,
"family_name": req.Identity.FamilyName,
"date_of_birth": req.Identity.DateOfBirth,
"tax_id": req.Identity.TaxID,
@@ -713,18 +713,18 @@ func TestListAssets_FIDescriptionParsed(t *testing.T) {
json.NewEncoder(w).Encode(map[string]interface{}{
"us_treasuries": []map[string]interface{}{
{
"cusip": "912540ZA2",
"isin": "US912540ZA28",
"tradable": true,
"bond_status": "outstanding",
"subtype": "bond",
"description": "United States Treasury 0.0%, 01/01/2056",
"cusip": "912540ZA2",
"isin": "US912540ZA28",
"tradable": true,
"bond_status": "outstanding",
"subtype": "bond",
"description": "United States Treasury 0.0%, 01/01/2056",
"description_short": "UST 0.0% 01/01/2056",
"coupon": 0,
"coupon_type": "zero",
"coupon_frequency": "zero",
"maturity_date": "2056-01-01",
"issue_date": "2026-01-01",
"coupon": 0,
"coupon_type": "zero",
"coupon_frequency": "zero",
"maturity_date": "2056-01-01",
"issue_date": "2026-01-01",
},
},
})
+3 -3
View File
@@ -57,9 +57,9 @@ type jwtSigner struct {
tokenURL string
httpClient *http.Client
mu sync.Mutex
tokenVal string
tokenExp time.Time
mu sync.Mutex
tokenVal string
tokenExp time.Time
}
// newJWTSigner parses the PEM private key and returns a signer bound to the
+14 -14
View File
@@ -188,11 +188,11 @@ func (p *Provider) CreateOrder(ctx context.Context, _ string, req *types.CreateO
return nil, err
}
var raw struct {
OrderID int `json:"orderId"`
Symbol string `json:"symbol"`
Status string `json:"status"`
ExecutedQty string `json:"executedQty"`
CumQuoteQty string `json:"cummulativeQuoteQty"`
OrderID int `json:"orderId"`
Symbol string `json:"symbol"`
Status string `json:"status"`
ExecutedQty string `json:"executedQty"`
CumQuoteQty string `json:"cummulativeQuoteQty"`
}
json.Unmarshal(data, &raw)
return &types.Order{
@@ -314,13 +314,13 @@ func (p *Provider) GetSnapshot(ctx context.Context, symbol string) (*types.Marke
return nil, err
}
var raw struct {
BidPrice string `json:"bidPrice"`
AskPrice string `json:"askPrice"`
LastPrice string `json:"lastPrice"`
Volume string `json:"volume"`
High string `json:"highPrice"`
Low string `json:"lowPrice"`
Open string `json:"openPrice"`
BidPrice string `json:"bidPrice"`
AskPrice string `json:"askPrice"`
LastPrice string `json:"lastPrice"`
Volume string `json:"volume"`
High string `json:"highPrice"`
Low string `json:"lowPrice"`
Open string `json:"openPrice"`
}
json.Unmarshal(data, &raw)
bid, _ := strconv.ParseFloat(raw.BidPrice, 64)
@@ -335,11 +335,11 @@ func (p *Provider) GetSnapshot(ctx context.Context, symbol string) (*types.Marke
Symbol: symbol,
LatestQuote: &types.Quote{
Timestamp: time.Now().UTC().Format(time.RFC3339),
BidPrice: bid, AskPrice: ask,
BidPrice: bid, AskPrice: ask,
},
LatestTrade: &types.Trade{
Timestamp: time.Now().UTC().Format(time.RFC3339),
Price: last,
Price: last,
},
DailyBar: &types.Bar{Open: op, High: hi, Low: lo, Close: last, Volume: vol},
}, nil
+3 -3
View File
@@ -155,9 +155,9 @@ func (p *Provider) GetPortfolio(ctx context.Context, providerAccountID string) (
PortfolioValue: fmt.Sprintf("%d", wallet.Balance),
Positions: []types.Position{
{
Symbol: wallet.Coin,
Qty: fmt.Sprintf("%d", wallet.Balance),
AssetClass: "crypto",
Symbol: wallet.Coin,
Qty: fmt.Sprintf("%d", wallet.Balance),
AssetClass: "crypto",
},
},
}, nil
+125 -34
View File
@@ -19,10 +19,15 @@ const (
type Config struct{ BaseURL, APIKey string }
type Provider struct{ cfg Config; client *http.Client }
type Provider struct {
cfg Config
client *http.Client
}
func New(cfg Config) *Provider {
if cfg.BaseURL == "" { cfg.BaseURL = ProdURL }
if cfg.BaseURL == "" {
cfg.BaseURL = ProdURL
}
return &Provider{cfg: cfg, client: &http.Client{Timeout: 30 * time.Second}}
}
@@ -32,88 +37,160 @@ var errNotSupported = fmt.Errorf("not supported by circle")
func (p *Provider) doRequest(ctx context.Context, method, path string, body interface{}) ([]byte, error) {
var reqBody io.Reader
if body != nil { b, _ := json.Marshal(body); reqBody = bytes.NewReader(b) }
if body != nil {
b, _ := json.Marshal(body)
reqBody = bytes.NewReader(b)
}
req, err := http.NewRequestWithContext(ctx, method, p.cfg.BaseURL+path, reqBody)
if err != nil { return nil, err }
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+p.cfg.APIKey)
req.Header.Set("Content-Type", "application/json")
resp, err := p.client.Do(req)
if err != nil { return nil, err }
if err != nil {
return nil, err
}
defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body)
if resp.StatusCode >= 400 { return nil, fmt.Errorf("circle error %d: %s", resp.StatusCode, string(data)) }
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("circle error %d: %s", resp.StatusCode, string(data))
}
return data, nil
}
func (p *Provider) CreateAccount(ctx context.Context, req *types.CreateAccountRequest) (*types.Account, error) {
body := map[string]interface{}{"idempotencyKey": fmt.Sprintf("lux_%d", time.Now().UnixNano()), "description": req.Identity.GivenName}
data, err := p.doRequest(ctx, "POST", "/v1/businessAccount/wallets", body)
if err != nil { return nil, err }
var resp struct{ Data struct{ WalletID string `json:"walletId"` } `json:"data"` }
if err != nil {
return nil, err
}
var resp struct {
Data struct {
WalletID string `json:"walletId"`
} `json:"data"`
}
json.Unmarshal(data, &resp)
return &types.Account{ID: resp.Data.WalletID, Provider: "circle", ProviderID: resp.Data.WalletID, Status: "active", Currency: "USD", CreatedAt: time.Now(), UpdatedAt: time.Now()}, nil
}
func (p *Provider) GetAccount(ctx context.Context, id string) (*types.Account, error) {
data, err := p.doRequest(ctx, "GET", "/v1/businessAccount/wallets/"+id, nil)
if err != nil { return nil, err }
var resp struct{ Data struct{ WalletID string `json:"walletId"`; EntityID string `json:"entityId"` } `json:"data"` }
if err != nil {
return nil, err
}
var resp struct {
Data struct {
WalletID string `json:"walletId"`
EntityID string `json:"entityId"`
} `json:"data"`
}
json.Unmarshal(data, &resp)
return &types.Account{ID: resp.Data.WalletID, Provider: "circle", ProviderID: resp.Data.WalletID, Status: "active"}, nil
}
func (p *Provider) ListAccounts(ctx context.Context) ([]*types.Account, error) {
data, err := p.doRequest(ctx, "GET", "/v1/businessAccount/wallets", nil)
if err != nil { return nil, err }
var resp struct{ Data []struct{ WalletID string `json:"walletId"` } `json:"data"` }
if err != nil {
return nil, err
}
var resp struct {
Data []struct {
WalletID string `json:"walletId"`
} `json:"data"`
}
json.Unmarshal(data, &resp)
accts := make([]*types.Account, 0, len(resp.Data))
for _, w := range resp.Data { accts = append(accts, &types.Account{ID: w.WalletID, Provider: "circle", ProviderID: w.WalletID, Status: "active"}) }
for _, w := range resp.Data {
accts = append(accts, &types.Account{ID: w.WalletID, Provider: "circle", ProviderID: w.WalletID, Status: "active"})
}
return accts, nil
}
func (p *Provider) GetPortfolio(ctx context.Context, id string) (*types.Portfolio, error) {
data, err := p.doRequest(ctx, "GET", "/v1/businessAccount/balances", nil)
if err != nil { return nil, err }
var resp struct{ Data struct{ Available []struct{ Amount string `json:"amount"`; Currency string `json:"currency"` } `json:"available"` } `json:"data"` }
if err != nil {
return nil, err
}
var resp struct {
Data struct {
Available []struct {
Amount string `json:"amount"`
Currency string `json:"currency"`
} `json:"available"`
} `json:"data"`
}
json.Unmarshal(data, &resp)
positions := make([]types.Position, 0)
for _, b := range resp.Data.Available { positions = append(positions, types.Position{Symbol: b.Currency, Qty: b.Amount, AssetClass: "crypto"}) }
for _, b := range resp.Data.Available {
positions = append(positions, types.Position{Symbol: b.Currency, Qty: b.Amount, AssetClass: "crypto"})
}
return &types.Portfolio{AccountID: id, Positions: positions}, nil
}
func (p *Provider) CreateOrder(ctx context.Context, accountID string, req *types.CreateOrderRequest) (*types.Order, error) {
body := map[string]interface{}{"idempotencyKey": fmt.Sprintf("lux_%d", time.Now().UnixNano()), "source": map[string]string{"type": "wallet", "id": accountID}, "amount": map[string]string{"amount": req.Qty, "currency": "USD"}, "destination": map[string]string{"type": "blockchain", "chain": "ETH"}}
data, err := p.doRequest(ctx, "POST", "/v1/businessAccount/transfers", body)
if err != nil { return nil, err }
var resp struct{ Data struct{ ID string `json:"id"`; Status string `json:"status"` } `json:"data"` }
if err != nil {
return nil, err
}
var resp struct {
Data struct {
ID string `json:"id"`
Status string `json:"status"`
} `json:"data"`
}
json.Unmarshal(data, &resp)
return &types.Order{ID: resp.Data.ID, Provider: "circle", ProviderID: resp.Data.ID, AccountID: accountID, Symbol: req.Symbol, Qty: req.Qty, Side: req.Side, Status: resp.Data.Status, CreatedAt: time.Now()}, nil
}
func (p *Provider) ListOrders(ctx context.Context, accountID string) ([]*types.Order, error) {
data, err := p.doRequest(ctx, "GET", "/v1/businessAccount/transfers", nil)
if err != nil { return nil, err }
var resp struct{ Data []struct{ ID string `json:"id"`; Status string `json:"status"` } `json:"data"` }
if err != nil {
return nil, err
}
var resp struct {
Data []struct {
ID string `json:"id"`
Status string `json:"status"`
} `json:"data"`
}
json.Unmarshal(data, &resp)
orders := make([]*types.Order, 0, len(resp.Data))
for _, t := range resp.Data { orders = append(orders, &types.Order{ID: t.ID, Provider: "circle", ProviderID: t.ID, Status: t.Status}) }
for _, t := range resp.Data {
orders = append(orders, &types.Order{ID: t.ID, Provider: "circle", ProviderID: t.ID, Status: t.Status})
}
return orders, nil
}
func (p *Provider) GetOrder(ctx context.Context, accountID, orderID string) (*types.Order, error) {
data, err := p.doRequest(ctx, "GET", "/v1/businessAccount/transfers/"+orderID, nil)
if err != nil { return nil, err }
var resp struct{ Data struct{ ID string `json:"id"`; Status string `json:"status"` } `json:"data"` }
if err != nil {
return nil, err
}
var resp struct {
Data struct {
ID string `json:"id"`
Status string `json:"status"`
} `json:"data"`
}
json.Unmarshal(data, &resp)
return &types.Order{ID: resp.Data.ID, Provider: "circle", ProviderID: resp.Data.ID, Status: resp.Data.Status}, nil
}
func (p *Provider) CancelOrder(_ context.Context, _, _ string) error { return errNotSupported }
func (p *Provider) CreateTransfer(_ context.Context, _ string, _ *types.CreateTransferRequest) (*types.Transfer, error) { return nil, errNotSupported }
func (p *Provider) ListTransfers(_ context.Context, _ string) ([]*types.Transfer, error) { return nil, errNotSupported }
func (p *Provider) CreateBankRelationship(_ context.Context, _, _, _, _, _ string) (*types.BankRelationship, error) { return nil, errNotSupported }
func (p *Provider) ListBankRelationships(_ context.Context, _ string) ([]*types.BankRelationship, error) { return nil, errNotSupported }
func (p *Provider) CreateTransfer(_ context.Context, _ string, _ *types.CreateTransferRequest) (*types.Transfer, error) {
return nil, errNotSupported
}
func (p *Provider) ListTransfers(_ context.Context, _ string) ([]*types.Transfer, error) {
return nil, errNotSupported
}
func (p *Provider) CreateBankRelationship(_ context.Context, _, _, _, _, _ string) (*types.BankRelationship, error) {
return nil, errNotSupported
}
func (p *Provider) ListBankRelationships(_ context.Context, _ string) ([]*types.BankRelationship, error) {
return nil, errNotSupported
}
func (p *Provider) ListAssets(_ context.Context, _ string) ([]*types.Asset, error) {
return []*types.Asset{
{ID: "USDC", Provider: "circle", Symbol: "USDC", Name: "USD Coin", Class: "crypto", Status: "active", Tradable: true},
@@ -123,10 +200,24 @@ func (p *Provider) ListAssets(_ context.Context, _ string) ([]*types.Asset, erro
func (p *Provider) GetAsset(_ context.Context, sym string) (*types.Asset, error) {
return &types.Asset{ID: sym, Provider: "circle", Symbol: sym, Class: "crypto", Status: "active", Tradable: true}, nil
}
func (p *Provider) GetSnapshot(_ context.Context, _ string) (*types.MarketSnapshot, error) { return nil, errNotSupported }
func (p *Provider) GetSnapshots(_ context.Context, _ []string) (map[string]*types.MarketSnapshot, error) { return nil, errNotSupported }
func (p *Provider) GetBars(_ context.Context, _, _, _, _ string, _ int) ([]*types.Bar, error) { return nil, errNotSupported }
func (p *Provider) GetLatestTrades(_ context.Context, _ []string) (map[string]*types.Trade, error) { return nil, errNotSupported }
func (p *Provider) GetLatestQuotes(_ context.Context, _ []string) (map[string]*types.Quote, error) { return nil, errNotSupported }
func (p *Provider) GetClock(_ context.Context) (*types.MarketClock, error) { return nil, errNotSupported }
func (p *Provider) GetCalendar(_ context.Context, _, _ string) ([]*types.MarketCalendarDay, error) { return nil, errNotSupported }
func (p *Provider) GetSnapshot(_ context.Context, _ string) (*types.MarketSnapshot, error) {
return nil, errNotSupported
}
func (p *Provider) GetSnapshots(_ context.Context, _ []string) (map[string]*types.MarketSnapshot, error) {
return nil, errNotSupported
}
func (p *Provider) GetBars(_ context.Context, _, _, _, _ string, _ int) ([]*types.Bar, error) {
return nil, errNotSupported
}
func (p *Provider) GetLatestTrades(_ context.Context, _ []string) (map[string]*types.Trade, error) {
return nil, errNotSupported
}
func (p *Provider) GetLatestQuotes(_ context.Context, _ []string) (map[string]*types.Quote, error) {
return nil, errNotSupported
}
func (p *Provider) GetClock(_ context.Context) (*types.MarketClock, error) {
return nil, errNotSupported
}
func (p *Provider) GetCalendar(_ context.Context, _, _ string) ([]*types.MarketCalendarDay, error) {
return nil, errNotSupported
}
+8 -8
View File
@@ -232,19 +232,19 @@ func (p *Provider) GetOrder(ctx context.Context, _ string, orderID string) (*typ
}
var raw struct {
Order struct {
OrderID string `json:"order_id"`
ProductID string `json:"product_id"`
Side string `json:"side"`
Status string `json:"status"`
FilledSize string `json:"filled_size"`
AveragePrice string `json:"average_filled_price"`
OrderID string `json:"order_id"`
ProductID string `json:"product_id"`
Side string `json:"side"`
Status string `json:"status"`
FilledSize string `json:"filled_size"`
AveragePrice string `json:"average_filled_price"`
} `json:"order"`
}
json.Unmarshal(data, &raw)
return &types.Order{
Provider: "coinbase", ProviderID: raw.Order.OrderID,
Symbol: raw.Order.ProductID, Side: strings.ToLower(raw.Order.Side),
Status: strings.ToLower(raw.Order.Status),
Status: strings.ToLower(raw.Order.Status),
FilledQty: raw.Order.FilledSize, FilledAvgPrice: raw.Order.AveragePrice,
}, nil
}
@@ -295,7 +295,7 @@ func (p *Provider) ListAssets(ctx context.Context, _ string) ([]*types.Asset, er
assets = append(assets, &types.Asset{
ID: prod.ProductID, Provider: "coinbase",
Symbol: prod.BaseCurrency + "/" + prod.QuoteCurrency,
Name: prod.ProductID, Class: "crypto",
Name: prod.ProductID, Class: "crypto",
Status: "active", Tradable: true, Fractionable: true,
})
}
+40 -40
View File
@@ -22,7 +22,7 @@ import (
// Config holds the common credentials and endpoints for a crypto exchange.
type Config struct {
Name string `json:"name"` // exchange identifier (e.g. "myexchange")
Name string `json:"name"` // exchange identifier (e.g. "myexchange")
BaseURL string `json:"base_url"`
APIKey string `json:"api_key"`
APISecret string `json:"api_secret"`
@@ -33,59 +33,59 @@ type Config struct {
// Endpoints maps logical operations to concrete REST paths.
// Override these for each exchange.
type Endpoints struct {
CreateAccount string // POST
GetAccount string // GET, {account_id}
ListAccounts string // GET
GetPortfolio string // GET, {account_id}
CreateOrder string // POST
ListOrders string // GET, {account_id}
GetOrder string // GET, {account_id}, {order_id}
CancelOrder string // DELETE, {account_id}, {order_id}
ListAssets string // GET
GetAsset string // GET, {symbol}
GetSnapshot string // GET, {symbol}
GetSnapshots string // GET, ?symbols=
GetBars string // GET, {symbol}
GetLatestTrades string // GET
GetLatestQuotes string // GET
GetClock string // GET
GetCalendar string // GET
CreateTransfer string // POST
ListTransfers string // GET
CreateBankRelation string // POST
ListBankRelations string // GET
CreateAccount string // POST
GetAccount string // GET, {account_id}
ListAccounts string // GET
GetPortfolio string // GET, {account_id}
CreateOrder string // POST
ListOrders string // GET, {account_id}
GetOrder string // GET, {account_id}, {order_id}
CancelOrder string // DELETE, {account_id}, {order_id}
ListAssets string // GET
GetAsset string // GET, {symbol}
GetSnapshot string // GET, {symbol}
GetSnapshots string // GET, ?symbols=
GetBars string // GET, {symbol}
GetLatestTrades string // GET
GetLatestQuotes string // GET
GetClock string // GET
GetCalendar string // GET
CreateTransfer string // POST
ListTransfers string // GET
CreateBankRelation string // POST
ListBankRelations string // GET
}
// DefaultEndpoints returns sensible defaults that match the most common
// crypto exchange REST API patterns.
func DefaultEndpoints() Endpoints {
return Endpoints{
ListAccounts: "/api/v1/accounts",
GetAccount: "/api/v1/accounts/{account_id}",
GetPortfolio: "/api/v1/accounts/{account_id}/portfolio",
CreateOrder: "/api/v1/orders",
ListOrders: "/api/v1/orders",
GetOrder: "/api/v1/orders/{order_id}",
CancelOrder: "/api/v1/orders/{order_id}",
ListAssets: "/api/v1/assets",
GetAsset: "/api/v1/assets/{symbol}",
GetSnapshot: "/api/v1/ticker/{symbol}",
GetSnapshots: "/api/v1/tickers",
GetBars: "/api/v1/candles/{symbol}",
ListAccounts: "/api/v1/accounts",
GetAccount: "/api/v1/accounts/{account_id}",
GetPortfolio: "/api/v1/accounts/{account_id}/portfolio",
CreateOrder: "/api/v1/orders",
ListOrders: "/api/v1/orders",
GetOrder: "/api/v1/orders/{order_id}",
CancelOrder: "/api/v1/orders/{order_id}",
ListAssets: "/api/v1/assets",
GetAsset: "/api/v1/assets/{symbol}",
GetSnapshot: "/api/v1/ticker/{symbol}",
GetSnapshots: "/api/v1/tickers",
GetBars: "/api/v1/candles/{symbol}",
GetLatestTrades: "/api/v1/trades",
GetLatestQuotes: "/api/v1/quotes",
CreateTransfer: "/api/v1/transfers",
ListTransfers: "/api/v1/transfers",
CreateTransfer: "/api/v1/transfers",
ListTransfers: "/api/v1/transfers",
}
}
// BaseExchange implements provider.Provider with common crypto exchange patterns.
// Embed this in a concrete exchange provider and override methods as needed.
type BaseExchange struct {
Cfg Config
Eps Endpoints
Client *http.Client
SignFn func(method, path, body string, timestamp int64) string // custom signer
Cfg Config
Eps Endpoints
Client *http.Client
SignFn func(method, path, body string, timestamp int64) string // custom signer
}
// New creates a BaseExchange with default settings.
+63 -21
View File
@@ -38,24 +38,66 @@ func New(cfg Config) *Provider {
}
func (p *Provider) Name() string { return "cssf" }
func (p *Provider) CreateAccount(_ context.Context, _ *types.CreateAccountRequest) (*types.Account, error) { return nil, fmt.Errorf("cssf: CreateAccount not implemented") }
func (p *Provider) GetAccount(_ context.Context, _ string) (*types.Account, error) { return nil, fmt.Errorf("cssf: GetAccount not implemented") }
func (p *Provider) ListAccounts(_ context.Context) ([]*types.Account, error) { return nil, fmt.Errorf("cssf: ListAccounts not implemented") }
func (p *Provider) GetPortfolio(_ context.Context, _ string) (*types.Portfolio, error) { return nil, fmt.Errorf("cssf: GetPortfolio not implemented") }
func (p *Provider) CreateOrder(_ context.Context, _ string, _ *types.CreateOrderRequest) (*types.Order, error) { return nil, fmt.Errorf("cssf: CreateOrder not implemented") }
func (p *Provider) ListOrders(_ context.Context, _ string) ([]*types.Order, error) { return nil, fmt.Errorf("cssf: ListOrders not implemented") }
func (p *Provider) GetOrder(_ context.Context, _, _ string) (*types.Order, error) { return nil, fmt.Errorf("cssf: GetOrder not implemented") }
func (p *Provider) CancelOrder(_ context.Context, _, _ string) error { return fmt.Errorf("cssf: CancelOrder not implemented") }
func (p *Provider) CreateTransfer(_ context.Context, _ string, _ *types.CreateTransferRequest) (*types.Transfer, error) { return nil, fmt.Errorf("cssf: CreateTransfer not implemented") }
func (p *Provider) ListTransfers(_ context.Context, _ string) ([]*types.Transfer, error) { return nil, fmt.Errorf("cssf: ListTransfers not implemented") }
func (p *Provider) CreateBankRelationship(_ context.Context, _, _, _, _, _ string) (*types.BankRelationship, error) { return nil, fmt.Errorf("cssf: CreateBankRelationship not implemented") }
func (p *Provider) ListBankRelationships(_ context.Context, _ string) ([]*types.BankRelationship, error) { return nil, fmt.Errorf("cssf: ListBankRelationships not implemented") }
func (p *Provider) ListAssets(_ context.Context, _ string) ([]*types.Asset, error) { return nil, fmt.Errorf("cssf: ListAssets not implemented") }
func (p *Provider) GetAsset(_ context.Context, _ string) (*types.Asset, error) { return nil, fmt.Errorf("cssf: GetAsset not implemented") }
func (p *Provider) GetSnapshot(_ context.Context, _ string) (*types.MarketSnapshot, error) { return nil, fmt.Errorf("cssf: GetSnapshot not implemented") }
func (p *Provider) GetSnapshots(_ context.Context, _ []string) (map[string]*types.MarketSnapshot, error) { return nil, fmt.Errorf("cssf: GetSnapshots not implemented") }
func (p *Provider) GetBars(_ context.Context, _, _, _, _ string, _ int) ([]*types.Bar, error) { return nil, fmt.Errorf("cssf: GetBars not implemented") }
func (p *Provider) GetLatestTrades(_ context.Context, _ []string) (map[string]*types.Trade, error) { return nil, fmt.Errorf("cssf: GetLatestTrades not implemented") }
func (p *Provider) GetLatestQuotes(_ context.Context, _ []string) (map[string]*types.Quote, error) { return nil, fmt.Errorf("cssf: GetLatestQuotes not implemented") }
func (p *Provider) GetClock(_ context.Context) (*types.MarketClock, error) { return nil, fmt.Errorf("cssf: GetClock not implemented") }
func (p *Provider) GetCalendar(_ context.Context, _, _ string) ([]*types.MarketCalendarDay, error) { return nil, fmt.Errorf("cssf: GetCalendar not implemented") }
func (p *Provider) CreateAccount(_ context.Context, _ *types.CreateAccountRequest) (*types.Account, error) {
return nil, fmt.Errorf("cssf: CreateAccount not implemented")
}
func (p *Provider) GetAccount(_ context.Context, _ string) (*types.Account, error) {
return nil, fmt.Errorf("cssf: GetAccount not implemented")
}
func (p *Provider) ListAccounts(_ context.Context) ([]*types.Account, error) {
return nil, fmt.Errorf("cssf: ListAccounts not implemented")
}
func (p *Provider) GetPortfolio(_ context.Context, _ string) (*types.Portfolio, error) {
return nil, fmt.Errorf("cssf: GetPortfolio not implemented")
}
func (p *Provider) CreateOrder(_ context.Context, _ string, _ *types.CreateOrderRequest) (*types.Order, error) {
return nil, fmt.Errorf("cssf: CreateOrder not implemented")
}
func (p *Provider) ListOrders(_ context.Context, _ string) ([]*types.Order, error) {
return nil, fmt.Errorf("cssf: ListOrders not implemented")
}
func (p *Provider) GetOrder(_ context.Context, _, _ string) (*types.Order, error) {
return nil, fmt.Errorf("cssf: GetOrder not implemented")
}
func (p *Provider) CancelOrder(_ context.Context, _, _ string) error {
return fmt.Errorf("cssf: CancelOrder not implemented")
}
func (p *Provider) CreateTransfer(_ context.Context, _ string, _ *types.CreateTransferRequest) (*types.Transfer, error) {
return nil, fmt.Errorf("cssf: CreateTransfer not implemented")
}
func (p *Provider) ListTransfers(_ context.Context, _ string) ([]*types.Transfer, error) {
return nil, fmt.Errorf("cssf: ListTransfers not implemented")
}
func (p *Provider) CreateBankRelationship(_ context.Context, _, _, _, _, _ string) (*types.BankRelationship, error) {
return nil, fmt.Errorf("cssf: CreateBankRelationship not implemented")
}
func (p *Provider) ListBankRelationships(_ context.Context, _ string) ([]*types.BankRelationship, error) {
return nil, fmt.Errorf("cssf: ListBankRelationships not implemented")
}
func (p *Provider) ListAssets(_ context.Context, _ string) ([]*types.Asset, error) {
return nil, fmt.Errorf("cssf: ListAssets not implemented")
}
func (p *Provider) GetAsset(_ context.Context, _ string) (*types.Asset, error) {
return nil, fmt.Errorf("cssf: GetAsset not implemented")
}
func (p *Provider) GetSnapshot(_ context.Context, _ string) (*types.MarketSnapshot, error) {
return nil, fmt.Errorf("cssf: GetSnapshot not implemented")
}
func (p *Provider) GetSnapshots(_ context.Context, _ []string) (map[string]*types.MarketSnapshot, error) {
return nil, fmt.Errorf("cssf: GetSnapshots not implemented")
}
func (p *Provider) GetBars(_ context.Context, _, _, _, _ string, _ int) ([]*types.Bar, error) {
return nil, fmt.Errorf("cssf: GetBars not implemented")
}
func (p *Provider) GetLatestTrades(_ context.Context, _ []string) (map[string]*types.Trade, error) {
return nil, fmt.Errorf("cssf: GetLatestTrades not implemented")
}
func (p *Provider) GetLatestQuotes(_ context.Context, _ []string) (map[string]*types.Quote, error) {
return nil, fmt.Errorf("cssf: GetLatestQuotes not implemented")
}
func (p *Provider) GetClock(_ context.Context) (*types.MarketClock, error) {
return nil, fmt.Errorf("cssf: GetClock not implemented")
}
func (p *Provider) GetCalendar(_ context.Context, _, _ string) ([]*types.MarketCalendarDay, error) {
return nil, fmt.Errorf("cssf: GetCalendar not implemented")
}
+141 -42
View File
@@ -32,7 +32,9 @@ type Provider struct {
}
func New(cfg Config) *Provider {
if cfg.BaseURL == "" { cfg.BaseURL = ProdURL }
if cfg.BaseURL == "" {
cfg.BaseURL = ProdURL
}
return &Provider{cfg: cfg, client: &http.Client{Timeout: 30 * time.Second}}
}
@@ -41,17 +43,27 @@ func (p *Provider) Name() string { return "currencycloud" }
var errNotSupported = fmt.Errorf("not supported by currencycloud")
func (p *Provider) authenticate(ctx context.Context) error {
if p.authToken != "" && time.Now().Before(p.tokenExp) { return nil }
if p.authToken != "" && time.Now().Before(p.tokenExp) {
return nil
}
form := url.Values{"login_id": {p.cfg.LoginID}, "api_key": {p.cfg.APIKey}}
req, err := http.NewRequestWithContext(ctx, "POST", p.cfg.BaseURL+"/v2/authenticate/api", strings.NewReader(form.Encode()))
if err != nil { return err }
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := p.client.Do(req)
if err != nil { return err }
if err != nil {
return err
}
defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body)
if resp.StatusCode >= 400 { return fmt.Errorf("currencycloud auth error %d: %s", resp.StatusCode, string(data)) }
var result struct{ AuthToken string `json:"auth_token"` }
if resp.StatusCode >= 400 {
return fmt.Errorf("currencycloud auth error %d: %s", resp.StatusCode, string(data))
}
var result struct {
AuthToken string `json:"auth_token"`
}
json.Unmarshal(data, &result)
p.authToken = result.AuthToken
p.tokenExp = time.Now().Add(25 * time.Minute) // tokens last 30min
@@ -59,7 +71,9 @@ func (p *Provider) authenticate(ctx context.Context) error {
}
func (p *Provider) doRequest(ctx context.Context, method, path string, body interface{}) ([]byte, error) {
if err := p.authenticate(ctx); err != nil { return nil, err }
if err := p.authenticate(ctx); err != nil {
return nil, err
}
var reqBody io.Reader
if body != nil {
if form, ok := body.(url.Values); ok {
@@ -70,32 +84,54 @@ func (p *Provider) doRequest(ctx context.Context, method, path string, body inte
}
}
req, err := http.NewRequestWithContext(ctx, method, p.cfg.BaseURL+path, reqBody)
if err != nil { return nil, err }
if err != nil {
return nil, err
}
req.Header.Set("X-Auth-Token", p.authToken)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := p.client.Do(req)
if err != nil { return nil, err }
if err != nil {
return nil, err
}
defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body)
if resp.StatusCode >= 400 { return nil, fmt.Errorf("currencycloud error %d: %s", resp.StatusCode, string(data)) }
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("currencycloud error %d: %s", resp.StatusCode, string(data))
}
return data, nil
}
// CurrencyCloud uses sub-accounts, not brokerage accounts
func (p *Provider) CreateAccount(_ context.Context, _ *types.CreateAccountRequest) (*types.Account, error) { return nil, errNotSupported }
func (p *Provider) CreateAccount(_ context.Context, _ *types.CreateAccountRequest) (*types.Account, error) {
return nil, errNotSupported
}
func (p *Provider) GetAccount(ctx context.Context, id string) (*types.Account, error) {
data, err := p.doRequest(ctx, "GET", "/v2/accounts/"+id, nil)
if err != nil { return nil, err }
var resp struct{ ID string `json:"id"`; AccountName string `json:"account_name"`; Status string `json:"status"` }
if err != nil {
return nil, err
}
var resp struct {
ID string `json:"id"`
AccountName string `json:"account_name"`
Status string `json:"status"`
}
json.Unmarshal(data, &resp)
return &types.Account{ID: resp.ID, Provider: "currencycloud", ProviderID: resp.ID, AccountNumber: resp.AccountName, Status: resp.Status}, nil
}
func (p *Provider) ListAccounts(ctx context.Context) ([]*types.Account, error) {
data, err := p.doRequest(ctx, "GET", "/v2/accounts/find", nil)
if err != nil { return nil, err }
var resp struct{ Accounts []struct{ ID string `json:"id"`; AccountName string `json:"account_name"`; Status string `json:"status"` } `json:"accounts"` }
if err != nil {
return nil, err
}
var resp struct {
Accounts []struct {
ID string `json:"id"`
AccountName string `json:"account_name"`
Status string `json:"status"`
} `json:"accounts"`
}
json.Unmarshal(data, &resp)
accts := make([]*types.Account, 0, len(resp.Accounts))
for _, a := range resp.Accounts {
@@ -106,8 +142,15 @@ func (p *Provider) ListAccounts(ctx context.Context) ([]*types.Account, error) {
func (p *Provider) GetPortfolio(ctx context.Context, id string) (*types.Portfolio, error) {
data, err := p.doRequest(ctx, "GET", "/v2/balances/find", nil)
if err != nil { return nil, err }
var resp struct{ Balances []struct{ Currency string `json:"currency"`; Amount string `json:"amount"` } `json:"balances"` }
if err != nil {
return nil, err
}
var resp struct {
Balances []struct {
Currency string `json:"currency"`
Amount string `json:"amount"`
} `json:"balances"`
}
json.Unmarshal(data, &resp)
positions := make([]types.Position, 0, len(resp.Balances))
for _, b := range resp.Balances {
@@ -119,27 +162,44 @@ func (p *Provider) GetPortfolio(ctx context.Context, id string) (*types.Portfoli
// FX "orders" are conversions in CurrencyCloud
func (p *Provider) CreateOrder(ctx context.Context, accountID string, req *types.CreateOrderRequest) (*types.Order, error) {
// Parse symbol as currency pair (e.g. GBPUSD -> buy GBP, sell USD)
if len(req.Symbol) != 6 { return nil, fmt.Errorf("symbol must be 6-char FX pair (e.g. GBPUSD)") }
if len(req.Symbol) != 6 {
return nil, fmt.Errorf("symbol must be 6-char FX pair (e.g. GBPUSD)")
}
buyCcy := req.Symbol[:3]
sellCcy := req.Symbol[3:]
form := url.Values{
"buy_currency": {buyCcy},
"sell_currency": {sellCcy},
"amount": {req.Qty},
"fixed_side": {"buy"},
"buy_currency": {buyCcy},
"sell_currency": {sellCcy},
"amount": {req.Qty},
"fixed_side": {"buy"},
"term_agreement": {"true"},
}
data, err := p.doRequest(ctx, "POST", "/v2/conversions/create", form)
if err != nil { return nil, err }
var resp struct{ ID string `json:"id"`; Status string `json:"status"`; ClientRate string `json:"client_rate"` }
if err != nil {
return nil, err
}
var resp struct {
ID string `json:"id"`
Status string `json:"status"`
ClientRate string `json:"client_rate"`
}
json.Unmarshal(data, &resp)
return &types.Order{ID: resp.ID, Provider: "currencycloud", ProviderID: resp.ID, AccountID: accountID, Symbol: req.Symbol, Qty: req.Qty, Side: req.Side, Type: "fx_spot", Status: resp.Status, FilledAvgPrice: resp.ClientRate, CreatedAt: time.Now()}, nil
}
func (p *Provider) ListOrders(ctx context.Context, accountID string) ([]*types.Order, error) {
data, err := p.doRequest(ctx, "GET", "/v2/conversions/find", nil)
if err != nil { return nil, err }
var resp struct{ Conversions []struct{ ID string `json:"id"`; BuyCurrency string `json:"buy_currency"`; SellCurrency string `json:"sell_currency"`; Status string `json:"status"` } `json:"conversions"` }
if err != nil {
return nil, err
}
var resp struct {
Conversions []struct {
ID string `json:"id"`
BuyCurrency string `json:"buy_currency"`
SellCurrency string `json:"sell_currency"`
Status string `json:"status"`
} `json:"conversions"`
}
json.Unmarshal(data, &resp)
orders := make([]*types.Order, 0, len(resp.Conversions))
for _, c := range resp.Conversions {
@@ -150,8 +210,14 @@ func (p *Provider) ListOrders(ctx context.Context, accountID string) ([]*types.O
func (p *Provider) GetOrder(ctx context.Context, accountID, orderID string) (*types.Order, error) {
data, err := p.doRequest(ctx, "GET", "/v2/conversions/"+orderID, nil)
if err != nil { return nil, err }
var resp struct{ ID string `json:"id"`; Status string `json:"status"`; ClientRate string `json:"client_rate"` }
if err != nil {
return nil, err
}
var resp struct {
ID string `json:"id"`
Status string `json:"status"`
ClientRate string `json:"client_rate"`
}
json.Unmarshal(data, &resp)
return &types.Order{ID: resp.ID, Provider: "currencycloud", ProviderID: resp.ID, Status: resp.Status, FilledAvgPrice: resp.ClientRate}, nil
}
@@ -161,10 +227,18 @@ func (p *Provider) CancelOrder(ctx context.Context, accountID, orderID string) e
return err
}
func (p *Provider) CreateTransfer(_ context.Context, _ string, _ *types.CreateTransferRequest) (*types.Transfer, error) { return nil, errNotSupported }
func (p *Provider) ListTransfers(_ context.Context, _ string) ([]*types.Transfer, error) { return nil, errNotSupported }
func (p *Provider) CreateBankRelationship(_ context.Context, _, _, _, _, _ string) (*types.BankRelationship, error) { return nil, errNotSupported }
func (p *Provider) ListBankRelationships(_ context.Context, _ string) ([]*types.BankRelationship, error) { return nil, errNotSupported }
func (p *Provider) CreateTransfer(_ context.Context, _ string, _ *types.CreateTransferRequest) (*types.Transfer, error) {
return nil, errNotSupported
}
func (p *Provider) ListTransfers(_ context.Context, _ string) ([]*types.Transfer, error) {
return nil, errNotSupported
}
func (p *Provider) CreateBankRelationship(_ context.Context, _, _, _, _, _ string) (*types.BankRelationship, error) {
return nil, errNotSupported
}
func (p *Provider) ListBankRelationships(_ context.Context, _ string) ([]*types.BankRelationship, error) {
return nil, errNotSupported
}
// FX pairs as assets
func (p *Provider) ListAssets(_ context.Context, _ string) ([]*types.Asset, error) {
@@ -185,12 +259,19 @@ func (p *Provider) GetAsset(_ context.Context, sym string) (*types.Asset, error)
// FX rates as market snapshots
func (p *Provider) GetSnapshot(ctx context.Context, symbol string) (*types.MarketSnapshot, error) {
if len(symbol) != 6 { return nil, fmt.Errorf("symbol must be 6-char FX pair") }
if len(symbol) != 6 {
return nil, fmt.Errorf("symbol must be 6-char FX pair")
}
pair := symbol[:3] + symbol[3:]
form := url.Values{"currency_pair": {pair}}
data, err := p.doRequest(ctx, "GET", "/v2/rates/detailed?currency_pair="+pair, form)
if err != nil { return nil, err }
var resp struct{ BidPrice float64 `json:"client_buy_price,string"`; OfferPrice float64 `json:"client_sell_price,string"` }
if err != nil {
return nil, err
}
var resp struct {
BidPrice float64 `json:"client_buy_price,string"`
OfferPrice float64 `json:"client_sell_price,string"`
}
json.Unmarshal(data, &resp)
mid := (resp.BidPrice + resp.OfferPrice) / 2
return &types.MarketSnapshot{Symbol: symbol, LatestQuote: &types.Quote{BidPrice: resp.BidPrice, AskPrice: resp.OfferPrice}, LatestTrade: &types.Trade{Price: mid}}, nil
@@ -200,26 +281,42 @@ func (p *Provider) GetSnapshots(ctx context.Context, symbols []string) (map[stri
result := make(map[string]*types.MarketSnapshot)
for _, sym := range symbols {
snap, err := p.GetSnapshot(ctx, sym)
if err == nil { result[sym] = snap }
if err == nil {
result[sym] = snap
}
}
return result, nil
}
func (p *Provider) GetBars(_ context.Context, _, _, _, _ string, _ int) ([]*types.Bar, error) { return nil, errNotSupported }
func (p *Provider) GetBars(_ context.Context, _, _, _, _ string, _ int) ([]*types.Bar, error) {
return nil, errNotSupported
}
func (p *Provider) GetLatestTrades(ctx context.Context, symbols []string) (map[string]*types.Trade, error) {
snaps, err := p.GetSnapshots(ctx, symbols)
if err != nil { return nil, err }
if err != nil {
return nil, err
}
result := make(map[string]*types.Trade)
for sym, snap := range snaps { if snap.LatestTrade != nil { result[sym] = snap.LatestTrade } }
for sym, snap := range snaps {
if snap.LatestTrade != nil {
result[sym] = snap.LatestTrade
}
}
return result, nil
}
func (p *Provider) GetLatestQuotes(ctx context.Context, symbols []string) (map[string]*types.Quote, error) {
snaps, err := p.GetSnapshots(ctx, symbols)
if err != nil { return nil, err }
if err != nil {
return nil, err
}
result := make(map[string]*types.Quote)
for sym, snap := range snaps { if snap.LatestQuote != nil { result[sym] = snap.LatestQuote } }
for sym, snap := range snaps {
if snap.LatestQuote != nil {
result[sym] = snap.LatestQuote
}
}
return result, nil
}
@@ -231,4 +328,6 @@ func (p *Provider) GetClock(_ context.Context) (*types.MarketClock, error) {
return &types.MarketClock{IsOpen: isOpen, Timestamp: now.Format(time.RFC3339)}, nil
}
func (p *Provider) GetCalendar(_ context.Context, _, _ string) ([]*types.MarketCalendarDay, error) { return nil, errNotSupported }
func (p *Provider) GetCalendar(_ context.Context, _, _ string) ([]*types.MarketCalendarDay, error) {
return nil, errNotSupported
}
+1 -1
View File
@@ -7,9 +7,9 @@ import (
"os"
"github.com/luxfi/broker/pkg/provider"
"github.com/luxfi/broker/pkg/provider/apex"
"github.com/luxfi/broker/pkg/provider/alpaca"
"github.com/luxfi/broker/pkg/provider/alpaca_omnisub"
"github.com/luxfi/broker/pkg/provider/apex"
"github.com/luxfi/broker/pkg/provider/binance"
"github.com/luxfi/broker/pkg/provider/bitgo"
"github.com/luxfi/broker/pkg/provider/circle"
+63 -21
View File
@@ -38,24 +38,66 @@ func New(cfg Config) *Provider {
}
func (p *Provider) Name() string { return "euronext" }
func (p *Provider) CreateAccount(_ context.Context, _ *types.CreateAccountRequest) (*types.Account, error) { return nil, fmt.Errorf("euronext: CreateAccount not implemented") }
func (p *Provider) GetAccount(_ context.Context, _ string) (*types.Account, error) { return nil, fmt.Errorf("euronext: GetAccount not implemented") }
func (p *Provider) ListAccounts(_ context.Context) ([]*types.Account, error) { return nil, fmt.Errorf("euronext: ListAccounts not implemented") }
func (p *Provider) GetPortfolio(_ context.Context, _ string) (*types.Portfolio, error) { return nil, fmt.Errorf("euronext: GetPortfolio not implemented") }
func (p *Provider) CreateOrder(_ context.Context, _ string, _ *types.CreateOrderRequest) (*types.Order, error) { return nil, fmt.Errorf("euronext: CreateOrder not implemented") }
func (p *Provider) ListOrders(_ context.Context, _ string) ([]*types.Order, error) { return nil, fmt.Errorf("euronext: ListOrders not implemented") }
func (p *Provider) GetOrder(_ context.Context, _, _ string) (*types.Order, error) { return nil, fmt.Errorf("euronext: GetOrder not implemented") }
func (p *Provider) CancelOrder(_ context.Context, _, _ string) error { return fmt.Errorf("euronext: CancelOrder not implemented") }
func (p *Provider) CreateTransfer(_ context.Context, _ string, _ *types.CreateTransferRequest) (*types.Transfer, error) { return nil, fmt.Errorf("euronext: CreateTransfer not implemented") }
func (p *Provider) ListTransfers(_ context.Context, _ string) ([]*types.Transfer, error) { return nil, fmt.Errorf("euronext: ListTransfers not implemented") }
func (p *Provider) CreateBankRelationship(_ context.Context, _, _, _, _, _ string) (*types.BankRelationship, error) { return nil, fmt.Errorf("euronext: CreateBankRelationship not implemented") }
func (p *Provider) ListBankRelationships(_ context.Context, _ string) ([]*types.BankRelationship, error) { return nil, fmt.Errorf("euronext: ListBankRelationships not implemented") }
func (p *Provider) ListAssets(_ context.Context, _ string) ([]*types.Asset, error) { return nil, fmt.Errorf("euronext: ListAssets not implemented") }
func (p *Provider) GetAsset(_ context.Context, _ string) (*types.Asset, error) { return nil, fmt.Errorf("euronext: GetAsset not implemented") }
func (p *Provider) GetSnapshot(_ context.Context, _ string) (*types.MarketSnapshot, error) { return nil, fmt.Errorf("euronext: GetSnapshot not implemented") }
func (p *Provider) GetSnapshots(_ context.Context, _ []string) (map[string]*types.MarketSnapshot, error) { return nil, fmt.Errorf("euronext: GetSnapshots not implemented") }
func (p *Provider) GetBars(_ context.Context, _, _, _, _ string, _ int) ([]*types.Bar, error) { return nil, fmt.Errorf("euronext: GetBars not implemented") }
func (p *Provider) GetLatestTrades(_ context.Context, _ []string) (map[string]*types.Trade, error) { return nil, fmt.Errorf("euronext: GetLatestTrades not implemented") }
func (p *Provider) GetLatestQuotes(_ context.Context, _ []string) (map[string]*types.Quote, error) { return nil, fmt.Errorf("euronext: GetLatestQuotes not implemented") }
func (p *Provider) GetClock(_ context.Context) (*types.MarketClock, error) { return nil, fmt.Errorf("euronext: GetClock not implemented") }
func (p *Provider) GetCalendar(_ context.Context, _, _ string) ([]*types.MarketCalendarDay, error) { return nil, fmt.Errorf("euronext: GetCalendar not implemented") }
func (p *Provider) CreateAccount(_ context.Context, _ *types.CreateAccountRequest) (*types.Account, error) {
return nil, fmt.Errorf("euronext: CreateAccount not implemented")
}
func (p *Provider) GetAccount(_ context.Context, _ string) (*types.Account, error) {
return nil, fmt.Errorf("euronext: GetAccount not implemented")
}
func (p *Provider) ListAccounts(_ context.Context) ([]*types.Account, error) {
return nil, fmt.Errorf("euronext: ListAccounts not implemented")
}
func (p *Provider) GetPortfolio(_ context.Context, _ string) (*types.Portfolio, error) {
return nil, fmt.Errorf("euronext: GetPortfolio not implemented")
}
func (p *Provider) CreateOrder(_ context.Context, _ string, _ *types.CreateOrderRequest) (*types.Order, error) {
return nil, fmt.Errorf("euronext: CreateOrder not implemented")
}
func (p *Provider) ListOrders(_ context.Context, _ string) ([]*types.Order, error) {
return nil, fmt.Errorf("euronext: ListOrders not implemented")
}
func (p *Provider) GetOrder(_ context.Context, _, _ string) (*types.Order, error) {
return nil, fmt.Errorf("euronext: GetOrder not implemented")
}
func (p *Provider) CancelOrder(_ context.Context, _, _ string) error {
return fmt.Errorf("euronext: CancelOrder not implemented")
}
func (p *Provider) CreateTransfer(_ context.Context, _ string, _ *types.CreateTransferRequest) (*types.Transfer, error) {
return nil, fmt.Errorf("euronext: CreateTransfer not implemented")
}
func (p *Provider) ListTransfers(_ context.Context, _ string) ([]*types.Transfer, error) {
return nil, fmt.Errorf("euronext: ListTransfers not implemented")
}
func (p *Provider) CreateBankRelationship(_ context.Context, _, _, _, _, _ string) (*types.BankRelationship, error) {
return nil, fmt.Errorf("euronext: CreateBankRelationship not implemented")
}
func (p *Provider) ListBankRelationships(_ context.Context, _ string) ([]*types.BankRelationship, error) {
return nil, fmt.Errorf("euronext: ListBankRelationships not implemented")
}
func (p *Provider) ListAssets(_ context.Context, _ string) ([]*types.Asset, error) {
return nil, fmt.Errorf("euronext: ListAssets not implemented")
}
func (p *Provider) GetAsset(_ context.Context, _ string) (*types.Asset, error) {
return nil, fmt.Errorf("euronext: GetAsset not implemented")
}
func (p *Provider) GetSnapshot(_ context.Context, _ string) (*types.MarketSnapshot, error) {
return nil, fmt.Errorf("euronext: GetSnapshot not implemented")
}
func (p *Provider) GetSnapshots(_ context.Context, _ []string) (map[string]*types.MarketSnapshot, error) {
return nil, fmt.Errorf("euronext: GetSnapshots not implemented")
}
func (p *Provider) GetBars(_ context.Context, _, _, _, _ string, _ int) ([]*types.Bar, error) {
return nil, fmt.Errorf("euronext: GetBars not implemented")
}
func (p *Provider) GetLatestTrades(_ context.Context, _ []string) (map[string]*types.Trade, error) {
return nil, fmt.Errorf("euronext: GetLatestTrades not implemented")
}
func (p *Provider) GetLatestQuotes(_ context.Context, _ []string) (map[string]*types.Quote, error) {
return nil, fmt.Errorf("euronext: GetLatestQuotes not implemented")
}
func (p *Provider) GetClock(_ context.Context) (*types.MarketClock, error) {
return nil, fmt.Errorf("euronext: GetClock not implemented")
}
func (p *Provider) GetCalendar(_ context.Context, _, _ string) ([]*types.MarketCalendarDay, error) {
return nil, fmt.Errorf("euronext: GetCalendar not implemented")
}
+4 -4
View File
@@ -20,9 +20,9 @@ const (
// Config for FalconX provider.
// FalconX is an institutional crypto trading platform with RFQ (Request for Quote) model.
type Config struct {
BaseURL string `json:"base_url"`
APIKey string `json:"api_key"`
APISecret string `json:"api_secret"`
BaseURL string `json:"base_url"`
APIKey string `json:"api_key"`
APISecret string `json:"api_secret"`
Passphrase string `json:"passphrase"`
}
@@ -37,7 +37,7 @@ func New(cfg Config) *Provider {
cfg.BaseURL = SandboxURL
}
return &Provider{
cfg: cfg,
cfg: cfg,
client: &http.Client{Timeout: 30 * time.Second},
}
}
+4 -4
View File
@@ -80,10 +80,10 @@ func (p *Provider) CreateAccount(ctx context.Context, req *types.CreateAccountRe
// Create a Finix Identity (KYC entity)
identity := map[string]interface{}{
"entity": map[string]interface{}{
"first_name": req.Identity.GivenName,
"last_name": req.Identity.FamilyName,
"email": req.Contact.Email,
"phone": req.Contact.Phone,
"first_name": req.Identity.GivenName,
"last_name": req.Identity.FamilyName,
"email": req.Contact.Email,
"phone": req.Contact.Phone,
"personal_address": map[string]interface{}{
"line1": firstOrEmpty(req.Contact.Street),
"city": req.Contact.City,
+57 -16
View File
@@ -75,7 +75,10 @@ func (p *Provider) CreateAccount(ctx context.Context, req *types.CreateAccountRe
if err != nil {
return nil, err
}
var resp struct{ ID string `json:"id"`; Name string `json:"name"` }
var resp struct {
ID string `json:"id"`
Name string `json:"name"`
}
json.Unmarshal(data, &resp)
return &types.Account{ID: resp.ID, Provider: "fireblocks", ProviderID: resp.ID, Status: "active", CreatedAt: time.Now(), UpdatedAt: time.Now()}, nil
}
@@ -85,7 +88,10 @@ func (p *Provider) GetAccount(ctx context.Context, id string) (*types.Account, e
if err != nil {
return nil, err
}
var resp struct{ ID string `json:"id"`; Name string `json:"name"` }
var resp struct {
ID string `json:"id"`
Name string `json:"name"`
}
json.Unmarshal(data, &resp)
return &types.Account{ID: resp.ID, Provider: "fireblocks", ProviderID: resp.ID, Status: "active"}, nil
}
@@ -95,7 +101,10 @@ func (p *Provider) ListAccounts(ctx context.Context) ([]*types.Account, error) {
if err != nil {
return nil, err
}
var items []struct{ ID string `json:"id"`; Name string `json:"name"` }
var items []struct {
ID string `json:"id"`
Name string `json:"name"`
}
json.Unmarshal(data, &items)
accounts := make([]*types.Account, 0, len(items))
for _, item := range items {
@@ -110,7 +119,11 @@ func (p *Provider) GetPortfolio(ctx context.Context, id string) (*types.Portfoli
return nil, err
}
var resp struct {
Assets []struct{ ID string `json:"id"`; Balance string `json:"balance"`; Available string `json:"available"` } `json:"assets"`
Assets []struct {
ID string `json:"id"`
Balance string `json:"balance"`
Available string `json:"available"`
} `json:"assets"`
}
json.Unmarshal(data, &resp)
positions := make([]types.Position, 0, len(resp.Assets))
@@ -123,14 +136,17 @@ func (p *Provider) GetPortfolio(ctx context.Context, id string) (*types.Portfoli
func (p *Provider) CreateOrder(ctx context.Context, accountID string, req *types.CreateOrderRequest) (*types.Order, error) {
body := map[string]interface{}{
"assetId": req.Symbol, "amount": req.Qty,
"source": map[string]string{"type": "VAULT_ACCOUNT", "id": accountID},
"source": map[string]string{"type": "VAULT_ACCOUNT", "id": accountID},
"operation": "TRANSFER",
}
data, err := p.doRequest(ctx, "POST", "/transactions", body)
if err != nil {
return nil, err
}
var resp struct{ ID string `json:"id"`; Status string `json:"status"` }
var resp struct {
ID string `json:"id"`
Status string `json:"status"`
}
json.Unmarshal(data, &resp)
return &types.Order{ID: resp.ID, Provider: "fireblocks", ProviderID: resp.ID, AccountID: accountID, Symbol: req.Symbol, Qty: req.Qty, Side: req.Side, Type: req.Type, Status: resp.Status, CreatedAt: time.Now()}, nil
}
@@ -140,7 +156,11 @@ func (p *Provider) ListOrders(ctx context.Context, accountID string) ([]*types.O
if err != nil {
return nil, err
}
var items []struct{ ID string `json:"id"`; Status string `json:"status"`; AssetID string `json:"assetId"` }
var items []struct {
ID string `json:"id"`
Status string `json:"status"`
AssetID string `json:"assetId"`
}
json.Unmarshal(data, &items)
orders := make([]*types.Order, 0, len(items))
for _, item := range items {
@@ -154,7 +174,10 @@ func (p *Provider) GetOrder(ctx context.Context, accountID, orderID string) (*ty
if err != nil {
return nil, err
}
var resp struct{ ID string `json:"id"`; Status string `json:"status"` }
var resp struct {
ID string `json:"id"`
Status string `json:"status"`
}
json.Unmarshal(data, &resp)
return &types.Order{ID: resp.ID, Provider: "fireblocks", ProviderID: resp.ID, Status: resp.Status}, nil
}
@@ -182,7 +205,11 @@ func (p *Provider) ListAssets(ctx context.Context, class string) ([]*types.Asset
if err != nil {
return nil, err
}
var items []struct{ ID string `json:"id"`; Name string `json:"name"`; Type string `json:"type"` }
var items []struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
}
json.Unmarshal(data, &items)
assets := make([]*types.Asset, 0, len(items))
for _, item := range items {
@@ -195,10 +222,24 @@ func (p *Provider) GetAsset(ctx context.Context, symbolOrID string) (*types.Asse
return &types.Asset{ID: symbolOrID, Provider: "fireblocks", Symbol: symbolOrID, Class: "crypto", Status: "active", Tradable: true}, nil
}
func (p *Provider) GetSnapshot(_ context.Context, _ string) (*types.MarketSnapshot, error) { return nil, errNotSupported }
func (p *Provider) GetSnapshots(_ context.Context, _ []string) (map[string]*types.MarketSnapshot, error) { return nil, errNotSupported }
func (p *Provider) GetBars(_ context.Context, _, _, _, _ string, _ int) ([]*types.Bar, error) { return nil, errNotSupported }
func (p *Provider) GetLatestTrades(_ context.Context, _ []string) (map[string]*types.Trade, error) { return nil, errNotSupported }
func (p *Provider) GetLatestQuotes(_ context.Context, _ []string) (map[string]*types.Quote, error) { return nil, errNotSupported }
func (p *Provider) GetClock(_ context.Context) (*types.MarketClock, error) { return nil, errNotSupported }
func (p *Provider) GetCalendar(_ context.Context, _, _ string) ([]*types.MarketCalendarDay, error) { return nil, errNotSupported }
func (p *Provider) GetSnapshot(_ context.Context, _ string) (*types.MarketSnapshot, error) {
return nil, errNotSupported
}
func (p *Provider) GetSnapshots(_ context.Context, _ []string) (map[string]*types.MarketSnapshot, error) {
return nil, errNotSupported
}
func (p *Provider) GetBars(_ context.Context, _, _, _, _ string, _ int) ([]*types.Bar, error) {
return nil, errNotSupported
}
func (p *Provider) GetLatestTrades(_ context.Context, _ []string) (map[string]*types.Trade, error) {
return nil, errNotSupported
}
func (p *Provider) GetLatestQuotes(_ context.Context, _ []string) (map[string]*types.Quote, error) {
return nil, errNotSupported
}
func (p *Provider) GetClock(_ context.Context) (*types.MarketClock, error) {
return nil, errNotSupported
}
func (p *Provider) GetCalendar(_ context.Context, _, _ string) ([]*types.MarketCalendarDay, error) {
return nil, errNotSupported
}
+13 -13
View File
@@ -146,10 +146,10 @@ func (p *Provider) GetPortfolio(ctx context.Context, _ string) (*types.Portfolio
func (p *Provider) CreateOrder(ctx context.Context, _ string, req *types.CreateOrderRequest) (*types.Order, error) {
sym := geminiSymbol(req.Symbol)
payload := map[string]interface{}{
"symbol": sym,
"amount": req.Qty,
"side": req.Side,
"type": "exchange " + req.Type, // Gemini: "exchange limit", "exchange market"
"symbol": sym,
"amount": req.Qty,
"side": req.Side,
"type": "exchange " + req.Type, // Gemini: "exchange limit", "exchange market"
}
if req.LimitPrice != "" {
payload["price"] = req.LimitPrice
@@ -164,16 +164,16 @@ func (p *Provider) CreateOrder(ctx context.Context, _ string, req *types.CreateO
return nil, err
}
var raw struct {
OrderID string `json:"order_id"`
Symbol string `json:"symbol"`
Side string `json:"side"`
Type string `json:"type"`
Price string `json:"price"`
AvgPrice string `json:"avg_execution_price"`
OrderID string `json:"order_id"`
Symbol string `json:"symbol"`
Side string `json:"side"`
Type string `json:"type"`
Price string `json:"price"`
AvgPrice string `json:"avg_execution_price"`
ExecutedAmount string `json:"executed_amount"`
OriginalAmount string `json:"original_amount"`
IsLive bool `json:"is_live"`
IsCancelled bool `json:"is_cancelled"`
IsLive bool `json:"is_live"`
IsCancelled bool `json:"is_cancelled"`
}
json.Unmarshal(data, &raw)
@@ -322,7 +322,7 @@ func (p *Provider) GetSnapshot(ctx context.Context, symbol string) (*types.Marke
Symbol: symbol,
LatestQuote: &types.Quote{
Timestamp: time.Now().UTC().Format(time.RFC3339),
BidPrice: bid, AskPrice: ask,
BidPrice: bid, AskPrice: ask,
},
LatestTrade: &types.Trade{Timestamp: time.Now().UTC().Format(time.RFC3339), Price: last},
DailyBar: &types.Bar{Close: last, Volume: vol},
+13 -4
View File
@@ -68,7 +68,11 @@ func (p *Provider) ListAssets(ctx context.Context, _ string) ([]*types.Asset, er
if err != nil {
return nil, err
}
var meta struct{ Universe []struct{ Name string `json:"name"` } `json:"universe"` }
var meta struct {
Universe []struct {
Name string `json:"name"`
} `json:"universe"`
}
if err := json.Unmarshal(data, &meta); err != nil {
return nil, fmt.Errorf("hyperliquid: parse meta: %w", err)
}
@@ -198,14 +202,17 @@ func (p *Provider) GetLatestTrades(ctx context.Context, symbols []string) (map[s
if err != nil {
continue
}
var tr []struct{ Px, Sz string; Time int64 `json:"time"` }
var tr []struct {
Px, Sz string
Time int64 `json:"time"`
}
if json.Unmarshal(data, &tr) != nil || len(tr) == 0 {
continue
}
t := tr[len(tr)-1]
out[sym] = &types.Trade{
Timestamp: time.UnixMilli(t.Time).UTC().Format(time.RFC3339),
Price: pf(t.Px), Size: pf(t.Sz), Exchange: "hyperliquid",
Price: pf(t.Px), Size: pf(t.Sz), Exchange: "hyperliquid",
}
}
return out, nil
@@ -349,7 +356,9 @@ func (p *Provider) CancelOrder(_ context.Context, _, _ string) error { return Er
func (p *Provider) CreateTransfer(_ context.Context, _ string, _ *types.CreateTransferRequest) (*types.Transfer, error) {
return nil, ErrNotImplemented
}
func (p *Provider) ListTransfers(_ context.Context, _ string) ([]*types.Transfer, error) { return nil, nil }
func (p *Provider) ListTransfers(_ context.Context, _ string) ([]*types.Transfer, error) {
return nil, nil
}
func (p *Provider) CreateBankRelationship(_ context.Context, _, _, _, _, _ string) (*types.BankRelationship, error) {
return nil, ErrNotImplemented
}
+8 -8
View File
@@ -21,10 +21,10 @@ const (
// Config for Interactive Brokers provider.
type Config struct {
GatewayURL string `json:"gateway_url"` // Client Portal Gateway URL
AccountID string `json:"account_id"` // Primary account
AccessToken string `json:"access_token"` // OAuth token (Web API)
ConsumerKey string `json:"consumer_key"` // OAuth consumer key
GatewayURL string `json:"gateway_url"` // Client Portal Gateway URL
AccountID string `json:"account_id"` // Primary account
AccessToken string `json:"access_token"` // OAuth token (Web API)
ConsumerKey string `json:"consumer_key"` // OAuth consumer key
}
// Provider implements the broker Provider interface for IBKR.
@@ -229,7 +229,7 @@ func (p *Provider) CreateOrder(ctx context.Context, providerAccountID string, re
}
var result []struct {
OrderID string `json:"order_id"`
OrderID string `json:"order_id"`
OrderStatus string `json:"order_status"`
}
json.Unmarshal(data, &result)
@@ -410,9 +410,9 @@ func (p *Provider) resolveConid(ctx context.Context, symbol string) (int, error)
return 0, err
}
var results []struct {
Conid int `json:"conid"`
Symbol string `json:"symbol"`
Exchange string `json:"description"` // exchange info in description
Conid int `json:"conid"`
Symbol string `json:"symbol"`
Exchange string `json:"description"` // exchange info in description
Sections []struct {
SecType string `json:"secType"`
Exchange string `json:"exchange"`
+64 -22
View File
@@ -39,25 +39,67 @@ func New(cfg Config) *Provider {
return &Provider{cfg: cfg, client: &http.Client{Timeout: 30 * time.Second}}
}
func (p *Provider) Name() string { return "ig" }
func (p *Provider) CreateAccount(_ context.Context, _ *types.CreateAccountRequest) (*types.Account, error) { return nil, fmt.Errorf("ig: CreateAccount not implemented") }
func (p *Provider) GetAccount(_ context.Context, _ string) (*types.Account, error) { return nil, fmt.Errorf("ig: GetAccount not implemented") }
func (p *Provider) ListAccounts(_ context.Context) ([]*types.Account, error) { return nil, fmt.Errorf("ig: ListAccounts not implemented") }
func (p *Provider) GetPortfolio(_ context.Context, _ string) (*types.Portfolio, error) { return nil, fmt.Errorf("ig: GetPortfolio not implemented") }
func (p *Provider) CreateOrder(_ context.Context, _ string, _ *types.CreateOrderRequest) (*types.Order, error) { return nil, fmt.Errorf("ig: CreateOrder not implemented") }
func (p *Provider) ListOrders(_ context.Context, _ string) ([]*types.Order, error) { return nil, fmt.Errorf("ig: ListOrders not implemented") }
func (p *Provider) GetOrder(_ context.Context, _, _ string) (*types.Order, error) { return nil, fmt.Errorf("ig: GetOrder not implemented") }
func (p *Provider) CancelOrder(_ context.Context, _, _ string) error { return fmt.Errorf("ig: CancelOrder not implemented") }
func (p *Provider) CreateTransfer(_ context.Context, _ string, _ *types.CreateTransferRequest) (*types.Transfer, error) { return nil, fmt.Errorf("ig: CreateTransfer not implemented") }
func (p *Provider) ListTransfers(_ context.Context, _ string) ([]*types.Transfer, error) { return nil, fmt.Errorf("ig: ListTransfers not implemented") }
func (p *Provider) CreateBankRelationship(_ context.Context, _, _, _, _, _ string) (*types.BankRelationship, error) { return nil, fmt.Errorf("ig: CreateBankRelationship not implemented") }
func (p *Provider) ListBankRelationships(_ context.Context, _ string) ([]*types.BankRelationship, error) { return nil, fmt.Errorf("ig: ListBankRelationships not implemented") }
func (p *Provider) ListAssets(_ context.Context, _ string) ([]*types.Asset, error) { return nil, fmt.Errorf("ig: ListAssets not implemented") }
func (p *Provider) GetAsset(_ context.Context, _ string) (*types.Asset, error) { return nil, fmt.Errorf("ig: GetAsset not implemented") }
func (p *Provider) GetSnapshot(_ context.Context, _ string) (*types.MarketSnapshot, error) { return nil, fmt.Errorf("ig: GetSnapshot not implemented") }
func (p *Provider) GetSnapshots(_ context.Context, _ []string) (map[string]*types.MarketSnapshot, error) { return nil, fmt.Errorf("ig: GetSnapshots not implemented") }
func (p *Provider) GetBars(_ context.Context, _, _, _, _ string, _ int) ([]*types.Bar, error) { return nil, fmt.Errorf("ig: GetBars not implemented") }
func (p *Provider) GetLatestTrades(_ context.Context, _ []string) (map[string]*types.Trade, error) { return nil, fmt.Errorf("ig: GetLatestTrades not implemented") }
func (p *Provider) GetLatestQuotes(_ context.Context, _ []string) (map[string]*types.Quote, error) { return nil, fmt.Errorf("ig: GetLatestQuotes not implemented") }
func (p *Provider) GetClock(_ context.Context) (*types.MarketClock, error) { return nil, fmt.Errorf("ig: GetClock not implemented") }
func (p *Provider) GetCalendar(_ context.Context, _, _ string) ([]*types.MarketCalendarDay, error) { return nil, fmt.Errorf("ig: GetCalendar not implemented") }
func (p *Provider) Name() string { return "ig" }
func (p *Provider) CreateAccount(_ context.Context, _ *types.CreateAccountRequest) (*types.Account, error) {
return nil, fmt.Errorf("ig: CreateAccount not implemented")
}
func (p *Provider) GetAccount(_ context.Context, _ string) (*types.Account, error) {
return nil, fmt.Errorf("ig: GetAccount not implemented")
}
func (p *Provider) ListAccounts(_ context.Context) ([]*types.Account, error) {
return nil, fmt.Errorf("ig: ListAccounts not implemented")
}
func (p *Provider) GetPortfolio(_ context.Context, _ string) (*types.Portfolio, error) {
return nil, fmt.Errorf("ig: GetPortfolio not implemented")
}
func (p *Provider) CreateOrder(_ context.Context, _ string, _ *types.CreateOrderRequest) (*types.Order, error) {
return nil, fmt.Errorf("ig: CreateOrder not implemented")
}
func (p *Provider) ListOrders(_ context.Context, _ string) ([]*types.Order, error) {
return nil, fmt.Errorf("ig: ListOrders not implemented")
}
func (p *Provider) GetOrder(_ context.Context, _, _ string) (*types.Order, error) {
return nil, fmt.Errorf("ig: GetOrder not implemented")
}
func (p *Provider) CancelOrder(_ context.Context, _, _ string) error {
return fmt.Errorf("ig: CancelOrder not implemented")
}
func (p *Provider) CreateTransfer(_ context.Context, _ string, _ *types.CreateTransferRequest) (*types.Transfer, error) {
return nil, fmt.Errorf("ig: CreateTransfer not implemented")
}
func (p *Provider) ListTransfers(_ context.Context, _ string) ([]*types.Transfer, error) {
return nil, fmt.Errorf("ig: ListTransfers not implemented")
}
func (p *Provider) CreateBankRelationship(_ context.Context, _, _, _, _, _ string) (*types.BankRelationship, error) {
return nil, fmt.Errorf("ig: CreateBankRelationship not implemented")
}
func (p *Provider) ListBankRelationships(_ context.Context, _ string) ([]*types.BankRelationship, error) {
return nil, fmt.Errorf("ig: ListBankRelationships not implemented")
}
func (p *Provider) ListAssets(_ context.Context, _ string) ([]*types.Asset, error) {
return nil, fmt.Errorf("ig: ListAssets not implemented")
}
func (p *Provider) GetAsset(_ context.Context, _ string) (*types.Asset, error) {
return nil, fmt.Errorf("ig: GetAsset not implemented")
}
func (p *Provider) GetSnapshot(_ context.Context, _ string) (*types.MarketSnapshot, error) {
return nil, fmt.Errorf("ig: GetSnapshot not implemented")
}
func (p *Provider) GetSnapshots(_ context.Context, _ []string) (map[string]*types.MarketSnapshot, error) {
return nil, fmt.Errorf("ig: GetSnapshots not implemented")
}
func (p *Provider) GetBars(_ context.Context, _, _, _, _ string, _ int) ([]*types.Bar, error) {
return nil, fmt.Errorf("ig: GetBars not implemented")
}
func (p *Provider) GetLatestTrades(_ context.Context, _ []string) (map[string]*types.Trade, error) {
return nil, fmt.Errorf("ig: GetLatestTrades not implemented")
}
func (p *Provider) GetLatestQuotes(_ context.Context, _ []string) (map[string]*types.Quote, error) {
return nil, fmt.Errorf("ig: GetLatestQuotes not implemented")
}
func (p *Provider) GetClock(_ context.Context) (*types.MarketClock, error) {
return nil, fmt.Errorf("ig: GetClock not implemented")
}
func (p *Provider) GetCalendar(_ context.Context, _, _ string) ([]*types.MarketCalendarDay, error) {
return nil, fmt.Errorf("ig: GetCalendar not implemented")
}
+12 -10
View File
@@ -165,8 +165,10 @@ func (p *Provider) CreateOrder(ctx context.Context, _ string, req *types.CreateO
return nil, err
}
var raw struct {
Descr struct{ Order string `json:"order"` } `json:"descr"`
TxID []string `json:"txid"`
Descr struct {
Order string `json:"order"`
} `json:"descr"`
TxID []string `json:"txid"`
}
json.Unmarshal(data, &raw)
orderID := ""
@@ -192,9 +194,9 @@ func (p *Provider) ListOrders(ctx context.Context, _ string) ([]*types.Order, er
Type string `json:"type"`
Order string `json:"ordertype"`
} `json:"descr"`
Vol string `json:"vol"`
VolExec string `json:"vol_exec"`
Status string `json:"status"`
Vol string `json:"vol"`
VolExec string `json:"vol_exec"`
Status string `json:"status"`
} `json:"open"`
}
json.Unmarshal(data, &raw)
@@ -218,8 +220,8 @@ func (p *Provider) GetOrder(ctx context.Context, _ string, orderID string) (*typ
}
var raw map[string]struct {
Descr struct {
Pair string `json:"pair"`
Type string `json:"type"`
Pair string `json:"pair"`
Type string `json:"type"`
} `json:"descr"`
Vol string `json:"vol"`
VolExec string `json:"vol_exec"`
@@ -266,7 +268,7 @@ func (p *Provider) ListAssets(ctx context.Context, _ string) ([]*types.Asset, er
return defaultAssets(), nil
}
var resp struct {
Error []string `json:"error"`
Error []string `json:"error"`
Result map[string]struct {
Base string `json:"base"`
Quote string `json:"quote"`
@@ -299,7 +301,7 @@ func (p *Provider) GetSnapshot(ctx context.Context, symbol string) (*types.Marke
return nil, err
}
var resp struct {
Error []string `json:"error"`
Error []string `json:"error"`
Result map[string]struct {
A []string `json:"a"` // ask [price, whole_lot_volume, lot_volume]
B []string `json:"b"` // bid
@@ -323,7 +325,7 @@ func (p *Provider) GetSnapshot(ctx context.Context, symbol string) (*types.Marke
Symbol: symbol,
LatestQuote: &types.Quote{
Timestamp: time.Now().UTC().Format(time.RFC3339),
BidPrice: bid, AskPrice: ask,
BidPrice: bid, AskPrice: ask,
},
LatestTrade: &types.Trade{Timestamp: time.Now().UTC().Format(time.RFC3339), Price: last},
DailyBar: &types.Bar{Open: op, High: hi, Low: lo, Close: last, Volume: vol},
+112 -32
View File
@@ -35,7 +35,9 @@ type Provider struct {
}
func New(cfg Config) *Provider {
if cfg.BaseURL == "" { cfg.BaseURL = ProdURL }
if cfg.BaseURL == "" {
cfg.BaseURL = ProdURL
}
return &Provider{cfg: cfg, client: &http.Client{Timeout: 30 * time.Second}}
}
@@ -45,9 +47,14 @@ var errNotSupported = fmt.Errorf("not supported by lmax")
func (p *Provider) doRequest(ctx context.Context, method, path string, body interface{}) ([]byte, error) {
var reqBody io.Reader
if body != nil { b, _ := json.Marshal(body); reqBody = bytes.NewReader(b) }
if body != nil {
b, _ := json.Marshal(body)
reqBody = bytes.NewReader(b)
}
req, err := http.NewRequestWithContext(ctx, method, p.cfg.BaseURL+path, reqBody)
if err != nil { return nil, err }
if err != nil {
return nil, err
}
if p.cfg.APIKey != "" {
req.Header.Set("Authorization", "Bearer "+p.cfg.APIKey)
}
@@ -57,34 +64,55 @@ func (p *Provider) doRequest(ctx context.Context, method, path string, body inte
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
resp, err := p.client.Do(req)
if err != nil { return nil, err }
if err != nil {
return nil, err
}
defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body)
if resp.StatusCode >= 400 { return nil, fmt.Errorf("lmax error %d: %s", resp.StatusCode, string(data)) }
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("lmax error %d: %s", resp.StatusCode, string(data))
}
return data, nil
}
// LMAX accounts are managed externally
func (p *Provider) CreateAccount(_ context.Context, _ *types.CreateAccountRequest) (*types.Account, error) { return nil, errNotSupported }
func (p *Provider) CreateAccount(_ context.Context, _ *types.CreateAccountRequest) (*types.Account, error) {
return nil, errNotSupported
}
func (p *Provider) GetAccount(ctx context.Context, id string) (*types.Account, error) {
data, err := p.doRequest(ctx, "GET", "/api/account", nil)
if err != nil { return nil, err }
var resp struct{ AccountID string `json:"accountId"`; Status string `json:"status"`; Currency string `json:"currency"` }
if err != nil {
return nil, err
}
var resp struct {
AccountID string `json:"accountId"`
Status string `json:"status"`
Currency string `json:"currency"`
}
json.Unmarshal(data, &resp)
return &types.Account{ID: resp.AccountID, Provider: "lmax", ProviderID: resp.AccountID, Status: resp.Status, Currency: resp.Currency}, nil
}
func (p *Provider) ListAccounts(ctx context.Context) ([]*types.Account, error) {
acct, err := p.GetAccount(ctx, "")
if err != nil { return nil, err }
if err != nil {
return nil, err
}
return []*types.Account{acct}, nil
}
func (p *Provider) GetPortfolio(ctx context.Context, id string) (*types.Portfolio, error) {
data, err := p.doRequest(ctx, "GET", "/api/account/balance", nil)
if err != nil { return nil, err }
var resp struct{ Cash float64 `json:"cash"`; Equity float64 `json:"equity"`; UnrealizedPL float64 `json:"unrealisedProfitAndLoss"`; Margin float64 `json:"margin"` }
if err != nil {
return nil, err
}
var resp struct {
Cash float64 `json:"cash"`
Equity float64 `json:"equity"`
UnrealizedPL float64 `json:"unrealisedProfitAndLoss"`
Margin float64 `json:"margin"`
}
json.Unmarshal(data, &resp)
return &types.Portfolio{
AccountID: id,
@@ -107,16 +135,27 @@ func (p *Provider) CreateOrder(ctx context.Context, accountID string, req *types
}
data, err := p.doRequest(ctx, "POST", "/api/order", body)
if err != nil { return nil, err }
var resp struct{ OrderID string `json:"orderId"`; Status string `json:"status"` }
if err != nil {
return nil, err
}
var resp struct {
OrderID string `json:"orderId"`
Status string `json:"status"`
}
json.Unmarshal(data, &resp)
return &types.Order{ID: resp.OrderID, Provider: "lmax", ProviderID: resp.OrderID, AccountID: accountID, Symbol: req.Symbol, Qty: req.Qty, Side: req.Side, Type: req.Type, Status: resp.Status, CreatedAt: time.Now()}, nil
}
func (p *Provider) ListOrders(ctx context.Context, accountID string) ([]*types.Order, error) {
data, err := p.doRequest(ctx, "GET", "/api/order/active", nil)
if err != nil { return nil, err }
var items []struct{ OrderID string `json:"orderId"`; InstrumentID string `json:"instrumentId"`; Status string `json:"status"` }
if err != nil {
return nil, err
}
var items []struct {
OrderID string `json:"orderId"`
InstrumentID string `json:"instrumentId"`
Status string `json:"status"`
}
json.Unmarshal(data, &items)
orders := make([]*types.Order, 0, len(items))
for _, item := range items {
@@ -127,8 +166,13 @@ func (p *Provider) ListOrders(ctx context.Context, accountID string) ([]*types.O
func (p *Provider) GetOrder(ctx context.Context, accountID, orderID string) (*types.Order, error) {
data, err := p.doRequest(ctx, "GET", "/api/order/"+orderID, nil)
if err != nil { return nil, err }
var resp struct{ OrderID string `json:"orderId"`; Status string `json:"status"` }
if err != nil {
return nil, err
}
var resp struct {
OrderID string `json:"orderId"`
Status string `json:"status"`
}
json.Unmarshal(data, &resp)
return &types.Order{ID: resp.OrderID, Provider: "lmax", ProviderID: resp.OrderID, Status: resp.Status}, nil
}
@@ -138,10 +182,18 @@ func (p *Provider) CancelOrder(ctx context.Context, accountID, orderID string) e
return err
}
func (p *Provider) CreateTransfer(_ context.Context, _ string, _ *types.CreateTransferRequest) (*types.Transfer, error) { return nil, errNotSupported }
func (p *Provider) ListTransfers(_ context.Context, _ string) ([]*types.Transfer, error) { return nil, errNotSupported }
func (p *Provider) CreateBankRelationship(_ context.Context, _, _, _, _, _ string) (*types.BankRelationship, error) { return nil, errNotSupported }
func (p *Provider) ListBankRelationships(_ context.Context, _ string) ([]*types.BankRelationship, error) { return nil, errNotSupported }
func (p *Provider) CreateTransfer(_ context.Context, _ string, _ *types.CreateTransferRequest) (*types.Transfer, error) {
return nil, errNotSupported
}
func (p *Provider) ListTransfers(_ context.Context, _ string) ([]*types.Transfer, error) {
return nil, errNotSupported
}
func (p *Provider) CreateBankRelationship(_ context.Context, _, _, _, _, _ string) (*types.BankRelationship, error) {
return nil, errNotSupported
}
func (p *Provider) ListBankRelationships(_ context.Context, _ string) ([]*types.BankRelationship, error) {
return nil, errNotSupported
}
// LMAX instruments
func (p *Provider) ListAssets(_ context.Context, class string) ([]*types.Asset, error) {
@@ -158,7 +210,9 @@ func (p *Provider) ListAssets(_ context.Context, class string) ([]*types.Asset,
}
assets := make([]*types.Asset, 0)
for _, i := range instruments {
if class != "" && i.cls != class { continue }
if class != "" && i.cls != class {
continue
}
assets = append(assets, &types.Asset{ID: i.sym, Provider: "lmax", Symbol: i.sym, Name: i.name, Class: i.cls, Status: "active", Tradable: true})
}
return assets, nil
@@ -170,10 +224,18 @@ func (p *Provider) GetAsset(_ context.Context, sym string) (*types.Asset, error)
func (p *Provider) GetSnapshot(ctx context.Context, symbol string) (*types.MarketSnapshot, error) {
data, err := p.doRequest(ctx, "GET", "/api/marketdata/orderbook/"+symbol, nil)
if err != nil { return nil, err }
if err != nil {
return nil, err
}
var resp struct {
Bids []struct{ Price float64 `json:"price"`; Qty float64 `json:"quantity"` } `json:"bids"`
Asks []struct{ Price float64 `json:"price"`; Qty float64 `json:"quantity"` } `json:"asks"`
Bids []struct {
Price float64 `json:"price"`
Qty float64 `json:"quantity"`
} `json:"bids"`
Asks []struct {
Price float64 `json:"price"`
Qty float64 `json:"quantity"`
} `json:"asks"`
}
json.Unmarshal(data, &resp)
snap := &types.MarketSnapshot{Symbol: symbol}
@@ -189,26 +251,42 @@ func (p *Provider) GetSnapshots(ctx context.Context, symbols []string) (map[stri
result := make(map[string]*types.MarketSnapshot)
for _, sym := range symbols {
snap, err := p.GetSnapshot(ctx, sym)
if err == nil { result[sym] = snap }
if err == nil {
result[sym] = snap
}
}
return result, nil
}
func (p *Provider) GetBars(_ context.Context, _, _, _, _ string, _ int) ([]*types.Bar, error) { return nil, errNotSupported }
func (p *Provider) GetBars(_ context.Context, _, _, _, _ string, _ int) ([]*types.Bar, error) {
return nil, errNotSupported
}
func (p *Provider) GetLatestTrades(ctx context.Context, symbols []string) (map[string]*types.Trade, error) {
snaps, err := p.GetSnapshots(ctx, symbols)
if err != nil { return nil, err }
if err != nil {
return nil, err
}
result := make(map[string]*types.Trade)
for sym, snap := range snaps { if snap.LatestTrade != nil { result[sym] = snap.LatestTrade } }
for sym, snap := range snaps {
if snap.LatestTrade != nil {
result[sym] = snap.LatestTrade
}
}
return result, nil
}
func (p *Provider) GetLatestQuotes(ctx context.Context, symbols []string) (map[string]*types.Quote, error) {
snaps, err := p.GetSnapshots(ctx, symbols)
if err != nil { return nil, err }
if err != nil {
return nil, err
}
result := make(map[string]*types.Quote)
for sym, snap := range snaps { if snap.LatestQuote != nil { result[sym] = snap.LatestQuote } }
for sym, snap := range snaps {
if snap.LatestQuote != nil {
result[sym] = snap.LatestQuote
}
}
return result, nil
}
@@ -218,4 +296,6 @@ func (p *Provider) GetClock(_ context.Context) (*types.MarketClock, error) {
return &types.MarketClock{IsOpen: true, Timestamp: now.Format(time.RFC3339)}, nil
}
func (p *Provider) GetCalendar(_ context.Context, _, _ string) ([]*types.MarketCalendarDay, error) { return nil, errNotSupported }
func (p *Provider) GetCalendar(_ context.Context, _, _ string) ([]*types.MarketCalendarDay, error) {
return nil, errNotSupported
}
+13 -13
View File
@@ -68,20 +68,20 @@ func startTestDEX(t *testing.T) (string, func()) {
case OpGetOrder:
account, orderID := readAccountOrderReq(root)
respBytes = buildOrderResp(OpGetOrder, order{
id: orderID,
clientID: "cli-1",
account: account,
symbol: "BTC/USD",
side: "buy",
orderType: "limit",
tif: "gtc",
status: "filled",
qty: 0.1,
filledQty: 0.1,
limitPrice: 100.0,
id: orderID,
clientID: "cli-1",
account: account,
symbol: "BTC/USD",
side: "buy",
orderType: "limit",
tif: "gtc",
status: "filled",
qty: 0.1,
filledQty: 0.1,
limitPrice: 100.0,
avgFillPrice: 100.25,
createdNs: 1_700_000_000_000_000_000,
filledNs: 1_700_000_000_500_000_000,
createdNs: 1_700_000_000_000_000_000,
filledNs: 1_700_000_000_500_000_000,
})
case OpCreateOrder:
+115 -91
View File
@@ -46,9 +46,9 @@ const (
// Status bits in the flags lower byte. The opcode occupies the upper byte.
const (
StatusOK uint16 = 0x0000
StatusErr uint16 = 0x0001
StatusNotImpl uint16 = 0x0002
StatusOK uint16 = 0x0000
StatusErr uint16 = 0x0001
StatusNotImpl uint16 = 0x0002
)
// packFlags encodes opcode + status into the 16-bit flags header field.
@@ -62,14 +62,16 @@ func unpackFlags(flags uint16) (op, status uint16) {
}
// ───────────────────────────────────────────────────────────────────────
// ErrorResp (flags.status != OK)
//
// ErrorResp (flags.status != OK)
//
// ───────────────────────────────────────────────────────────────────────
//
// offset type name
// ────── ────── ───────────
// 0 text code (8 bytes: 4 off + 4 len)
// 8 text message (8 bytes: 4 off + 4 len)
// 16 (total)
// offset type name
// ────── ────── ───────────
// 0 text code (8 bytes: 4 off + 4 len)
// 8 text message (8 bytes: 4 off + 4 len)
// 16 (total)
const (
errOffCode = 0
errOffMessage = 8
@@ -90,11 +92,13 @@ func readError(root zap.Object) (code, message string) {
}
// ───────────────────────────────────────────────────────────────────────
// SymbolReq — single-symbol request (GetSnapshot, GetAsset, GetBook)
//
// SymbolReq — single-symbol request (GetSnapshot, GetAsset, GetBook)
//
// ───────────────────────────────────────────────────────────────────────
//
// 0 text symbol (8)
// 8 (total)
// 0 text symbol (8)
// 8 (total)
const (
symReqOffSymbol = 0
symReqSize = 8
@@ -113,11 +117,13 @@ func readSymbolReq(root zap.Object) string {
}
// ───────────────────────────────────────────────────────────────────────
// ClassReq — asset class filter (ListAssets)
//
// ClassReq — asset class filter (ListAssets)
//
// ───────────────────────────────────────────────────────────────────────
//
// 0 text class (8)
// 8 (total)
// 0 text class (8)
// 8 (total)
const (
classReqOffClass = 0
classReqSize = 8
@@ -136,14 +142,16 @@ func readClassReq(root zap.Object) string {
}
// ───────────────────────────────────────────────────────────────────────
// SymbolsReq — multi-symbol request (GetSnapshots/Quotes/Trades)
//
// SymbolsReq — multi-symbol request (GetSnapshots/Quotes/Trades)
//
// ───────────────────────────────────────────────────────────────────────
//
// Symbols are packed as a single comma-separated text field. This keeps the
// wire layout compact without needing a heterogeneous list-of-text encoding.
//
// 0 text symbols (8) — CSV: "BTC/USD,ETH/USD,SOL/USD"
// 8 (total)
// 0 text symbols (8) — CSV: "BTC/USD,ETH/USD,SOL/USD"
// 8 (total)
const (
symsReqOffSymbols = 0
symsReqSize = 8
@@ -163,15 +171,17 @@ func readSymbolsReq(root zap.Object) []string {
}
// ───────────────────────────────────────────────────────────────────────
// BarsReq — historical bars query (GetBars)
//
// BarsReq — historical bars query (GetBars)
//
// ───────────────────────────────────────────────────────────────────────
//
// 0 text symbol (8)
// 8 text timeframe (8)
// 16 text start (8)
// 24 text end (8)
// 32 int64 limit (8)
// 40 (total)
// 0 text symbol (8)
// 8 text timeframe (8)
// 16 text start (8)
// 24 text end (8)
// 32 int64 limit (8)
// 40 (total)
const (
barsReqOffSymbol = 0
barsReqOffTimeframe = 8
@@ -209,11 +219,13 @@ func readBarsReq(root zap.Object) barsReq {
}
// ───────────────────────────────────────────────────────────────────────
// AccountReq — account-scoped request (GetPortfolio, ListOrders)
//
// AccountReq — account-scoped request (GetPortfolio, ListOrders)
//
// ───────────────────────────────────────────────────────────────────────
//
// 0 text account (8)
// 8 (total)
// 0 text account (8)
// 8 (total)
const (
acctReqOffAccount = 0
acctReqSize = 8
@@ -232,12 +244,14 @@ func readAccountReq(root zap.Object) string {
}
// ───────────────────────────────────────────────────────────────────────
// AccountOrderReq — (account, order_id) (GetOrder, CancelOrder)
//
// AccountOrderReq — (account, order_id) (GetOrder, CancelOrder)
//
// ───────────────────────────────────────────────────────────────────────
//
// 0 text account (8)
// 8 text order_id (8)
// 16 (total)
// 0 text account (8)
// 8 text order_id (8)
// 16 (total)
const (
acctOrderReqOffAccount = 0
acctOrderReqOffOrder = 8
@@ -258,20 +272,22 @@ func readAccountOrderReq(root zap.Object) (account, order string) {
}
// ───────────────────────────────────────────────────────────────────────
// CreateOrderReq — place an order
//
// CreateOrderReq — place an order
//
// ───────────────────────────────────────────────────────────────────────
//
// 0 text account (8)
// 8 text symbol (8)
// 16 text side (8) — "buy" | "sell"
// 24 text order_type (8) — "market" | "limit" | "stop" | "stop_limit"
// 32 text time_in_force (8) — "day" | "gtc" | "ioc" | "fok"
// 40 text client_order_id (8)
// 48 float64 qty (8)
// 56 float64 notional (8)
// 64 float64 limit_price (8)
// 72 float64 stop_price (8)
// 80 (total)
// 0 text account (8)
// 8 text symbol (8)
// 16 text side (8) — "buy" | "sell"
// 24 text order_type (8) — "market" | "limit" | "stop" | "stop_limit"
// 32 text time_in_force (8) — "day" | "gtc" | "ioc" | "fok"
// 40 text client_order_id (8)
// 48 float64 qty (8)
// 56 float64 notional (8)
// 64 float64 limit_price (8)
// 72 float64 stop_price (8)
// 80 (total)
const (
coReqOffAccount = 0
coReqOffSymbol = 8
@@ -324,19 +340,21 @@ func readCreateOrderReq(root zap.Object) createOrderReq {
}
// ───────────────────────────────────────────────────────────────────────
// SnapshotResp — single quote snapshot
//
// SnapshotResp — single quote snapshot
//
// ───────────────────────────────────────────────────────────────────────
//
// 0 text symbol (8)
// 8 float64 last_price (8)
// 16 float64 last_size (8)
// 24 int64 last_time_ns (8)
// 32 float64 bid_price (8)
// 40 float64 bid_size (8)
// 48 float64 ask_price (8)
// 56 float64 ask_size (8)
// 64 int64 quote_time_ns (8)
// 72 (total)
// 0 text symbol (8)
// 8 float64 last_price (8)
// 16 float64 last_size (8)
// 24 int64 last_time_ns (8)
// 32 float64 bid_price (8)
// 40 float64 bid_size (8)
// 48 float64 ask_price (8)
// 56 float64 ask_size (8)
// 64 int64 quote_time_ns (8)
// 72 (total)
const (
snapOffSymbol = 0
snapOffLastPrice = 8
@@ -351,10 +369,10 @@ const (
)
type snapshot struct {
symbol string
lastPrice, lastSize, bidPrice, bidSize float64
askPrice, askSize float64
lastNs, quoteNs int64
symbol string
lastPrice, lastSize, bidPrice, bidSize float64
askPrice, askSize float64
lastNs, quoteNs int64
}
// writeSnapshot writes a snapshot struct into b and returns its abs offset.
@@ -404,18 +422,20 @@ func readSnapshotRoot(root zap.Object) snapshot {
}
// ───────────────────────────────────────────────────────────────────────
// AssetResp — single asset record
//
// AssetResp — single asset record
//
// ───────────────────────────────────────────────────────────────────────
//
// 0 text id (8)
// 8 text symbol (8)
// 16 text name (8)
// 24 text class (8)
// 32 text exchange (8)
// 40 text status (8)
// 48 text provider (8)
// 56 bool tradable (1, padded to 8)
// 64 (total, 8-aligned)
// 0 text id (8)
// 8 text symbol (8)
// 16 text name (8)
// 24 text class (8)
// 32 text exchange (8)
// 40 text status (8)
// 48 text provider (8)
// 56 bool tradable (1, padded to 8)
// 64 (total, 8-aligned)
const (
assetOffID = 0
assetOffSymbol = 8
@@ -430,7 +450,7 @@ const (
type asset struct {
id, symbol, name, class, exchange, status, provider string
tradable bool
tradable bool
}
func buildAssetResp(a asset) []byte {
@@ -462,26 +482,28 @@ func readAssetRoot(root zap.Object) asset {
}
// ───────────────────────────────────────────────────────────────────────
// OrderResp — single order record
//
// OrderResp — single order record
//
// ───────────────────────────────────────────────────────────────────────
//
// 0 text id (8)
// 8 text client_order_id (8)
// 16 text account (8)
// 24 text symbol (8)
// 32 text side (8)
// 40 text order_type (8)
// 48 text time_in_force (8)
// 56 text status (8)
// 64 float64 qty (8)
// 72 float64 filled_qty (8)
// 80 float64 limit_price (8)
// 88 float64 stop_price (8)
// 96 float64 avg_fill_price (8)
// 104 int64 created_ns (8)
// 112 int64 updated_ns (8)
// 120 int64 filled_ns (8)
// 128 (total)
// 0 text id (8)
// 8 text client_order_id (8)
// 16 text account (8)
// 24 text symbol (8)
// 32 text side (8)
// 40 text order_type (8)
// 48 text time_in_force (8)
// 56 text status (8)
// 64 float64 qty (8)
// 72 float64 filled_qty (8)
// 80 float64 limit_price (8)
// 88 float64 stop_price (8)
// 96 float64 avg_fill_price (8)
// 104 int64 created_ns (8)
// 112 int64 updated_ns (8)
// 120 int64 filled_ns (8)
// 128 (total)
const (
ordOffID = 0
ordOffCliID = 8
@@ -574,11 +596,13 @@ func readOrderRoot(root zap.Object) order {
}
// ───────────────────────────────────────────────────────────────────────
// EmptyResp — acknowledgment payload (CancelOrder success, etc.)
//
// EmptyResp — acknowledgment payload (CancelOrder success, etc.)
//
// ───────────────────────────────────────────────────────────────────────
//
// 0 bool ok (1, padded to 8)
// 8 (total)
// 0 bool ok (1, padded to 8)
// 8 (total)
const (
emptyOffOK = 0
emptySize = 8