mirror of
https://github.com/luxfi/broker.git
synced 2026-09-24 00:25:18 +00:00
fix: gofmt -s across repo (CI format check)
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -123,4 +123,3 @@ func (s *Server) arbDetectorThreshold() float64 {
|
||||
}
|
||||
return 5 // default
|
||||
}
|
||||
|
||||
|
||||
+153
-153
@@ -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
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user