mirror of
https://github.com/luxfi/broker.git
synced 2026-09-24 00:25:18 +00:00
301 lines
8.3 KiB
Go
301 lines
8.3 KiB
Go
package falcon
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/luxfi/broker/pkg/types"
|
|
)
|
|
|
|
const (
|
|
SandboxURL = "https://api.sandbox.falconx.io"
|
|
ProdURL = "https://api.falconx.io"
|
|
)
|
|
|
|
// 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"`
|
|
Passphrase string `json:"passphrase"`
|
|
}
|
|
|
|
// Provider implements the broker Provider interface for FalconX.
|
|
type Provider struct {
|
|
cfg Config
|
|
client *http.Client
|
|
}
|
|
|
|
func New(cfg Config) *Provider {
|
|
if cfg.BaseURL == "" {
|
|
cfg.BaseURL = SandboxURL
|
|
}
|
|
return &Provider{
|
|
cfg: cfg,
|
|
client: &http.Client{Timeout: 30 * time.Second},
|
|
}
|
|
}
|
|
|
|
func (p *Provider) Name() string { return "falcon" }
|
|
|
|
func (p *Provider) do(ctx context.Context, method, path string, body interface{}) ([]byte, int, error) {
|
|
var reqBody io.Reader
|
|
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, 0, err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("FX-ACCESS-KEY", p.cfg.APIKey)
|
|
req.Header.Set("FX-ACCESS-PASSPHRASE", p.cfg.Passphrase)
|
|
// FalconX uses HMAC-SHA256 signature — simplified here
|
|
req.Header.Set("FX-ACCESS-SIGN", "")
|
|
req.Header.Set("FX-ACCESS-TIMESTAMP", fmt.Sprintf("%d", time.Now().Unix()))
|
|
|
|
resp, err := p.client.Do(req)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
defer resp.Body.Close()
|
|
data, _ := io.ReadAll(resp.Body)
|
|
if resp.StatusCode >= 400 {
|
|
return nil, resp.StatusCode, fmt.Errorf("falcon %d: %s", resp.StatusCode, string(data))
|
|
}
|
|
return data, resp.StatusCode, nil
|
|
}
|
|
|
|
// --- Accounts ---
|
|
|
|
func (p *Provider) CreateAccount(_ context.Context, _ *types.CreateAccountRequest) (*types.Account, error) {
|
|
return nil, fmt.Errorf("falcon: account creation handled via FalconX onboarding")
|
|
}
|
|
|
|
func (p *Provider) GetAccount(ctx context.Context, providerAccountID string) (*types.Account, error) {
|
|
data, _, err := p.do(ctx, http.MethodGet, "/v1/balances", nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
_ = data
|
|
return &types.Account{
|
|
Provider: "falcon",
|
|
ProviderID: providerAccountID,
|
|
Status: "active",
|
|
Currency: "USD",
|
|
}, nil
|
|
}
|
|
|
|
func (p *Provider) ListAccounts(ctx context.Context) ([]*types.Account, error) {
|
|
return []*types.Account{{
|
|
Provider: "falcon",
|
|
ProviderID: "default",
|
|
Status: "active",
|
|
Currency: "USD",
|
|
}}, nil
|
|
}
|
|
|
|
// --- Portfolio ---
|
|
|
|
func (p *Provider) GetPortfolio(ctx context.Context, _ string) (*types.Portfolio, error) {
|
|
data, _, err := p.do(ctx, http.MethodGet, "/v1/balances", nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var resp []struct {
|
|
Platform string `json:"platform"`
|
|
Token string `json:"token"`
|
|
Balance float64 `json:"balance"`
|
|
}
|
|
json.Unmarshal(data, &resp)
|
|
|
|
positions := make([]types.Position, len(resp))
|
|
for i, b := range resp {
|
|
positions[i] = types.Position{
|
|
Symbol: b.Token,
|
|
Qty: fmt.Sprintf("%.8f", b.Balance),
|
|
AssetClass: "crypto",
|
|
}
|
|
}
|
|
return &types.Portfolio{
|
|
AccountID: "default",
|
|
Positions: positions,
|
|
}, nil
|
|
}
|
|
|
|
// --- Orders (RFQ model) ---
|
|
|
|
func (p *Provider) CreateOrder(ctx context.Context, _ string, req *types.CreateOrderRequest) (*types.Order, error) {
|
|
// FalconX uses RFQ: get quote first, then execute
|
|
quoteBody := map[string]interface{}{
|
|
"token_pair": map[string]string{
|
|
"base_token": req.Symbol,
|
|
"quote_token": "USD",
|
|
},
|
|
"quantity": map[string]string{
|
|
"token": req.Symbol,
|
|
"value": req.Qty,
|
|
},
|
|
"side": req.Side,
|
|
}
|
|
|
|
data, _, err := p.do(ctx, http.MethodPost, "/v1/quotes", quoteBody)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var quote struct {
|
|
FxQuoteID string `json:"fx_quote_id"`
|
|
BuyPrice float64 `json:"buy_price"`
|
|
SellPrice float64 `json:"sell_price"`
|
|
Status string `json:"status"`
|
|
}
|
|
json.Unmarshal(data, "e)
|
|
|
|
// Execute the quote
|
|
execData, _, err := p.do(ctx, http.MethodPost, "/v1/quotes/execute", map[string]string{
|
|
"fx_quote_id": quote.FxQuoteID,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var exec struct {
|
|
FxQuoteID string `json:"fx_quote_id"`
|
|
Status string `json:"status"`
|
|
}
|
|
json.Unmarshal(execData, &exec)
|
|
|
|
return &types.Order{
|
|
Provider: "falcon",
|
|
ProviderID: exec.FxQuoteID,
|
|
Symbol: req.Symbol,
|
|
Qty: req.Qty,
|
|
Side: req.Side,
|
|
Type: "rfq",
|
|
Status: exec.Status,
|
|
CreatedAt: time.Now(),
|
|
}, nil
|
|
}
|
|
|
|
func (p *Provider) ListOrders(ctx context.Context, _ string) ([]*types.Order, error) {
|
|
data, _, err := p.do(ctx, http.MethodGet, "/v1/trades", nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var resp []struct {
|
|
FxQuoteID string `json:"fx_quote_id"`
|
|
BaseToken string `json:"base_token"`
|
|
Side string `json:"side"`
|
|
Quantity float64 `json:"quantity"`
|
|
Price float64 `json:"price"`
|
|
Status string `json:"status"`
|
|
Timestamp string `json:"t_execute"`
|
|
}
|
|
json.Unmarshal(data, &resp)
|
|
|
|
orders := make([]*types.Order, len(resp))
|
|
for i, t := range resp {
|
|
orders[i] = &types.Order{
|
|
Provider: "falcon",
|
|
ProviderID: t.FxQuoteID,
|
|
Symbol: t.BaseToken,
|
|
Side: t.Side,
|
|
Qty: fmt.Sprintf("%.8f", t.Quantity),
|
|
FilledAvgPrice: fmt.Sprintf("%.2f", t.Price),
|
|
Type: "rfq",
|
|
Status: t.Status,
|
|
}
|
|
}
|
|
return orders, nil
|
|
}
|
|
|
|
func (p *Provider) GetOrder(ctx context.Context, _ string, orderID string) (*types.Order, error) {
|
|
orders, err := p.ListOrders(ctx, "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, o := range orders {
|
|
if o.ProviderID == orderID {
|
|
return o, nil
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("falcon: order %s not found", orderID)
|
|
}
|
|
|
|
func (p *Provider) CancelOrder(_ context.Context, _, _ string) error {
|
|
return fmt.Errorf("falcon: RFQ orders cannot be cancelled after execution")
|
|
}
|
|
|
|
// --- Transfers / Bank / Assets ---
|
|
|
|
func (p *Provider) CreateTransfer(_ context.Context, _ string, _ *types.CreateTransferRequest) (*types.Transfer, error) {
|
|
return nil, fmt.Errorf("falcon: transfers handled via settlement")
|
|
}
|
|
|
|
func (p *Provider) ListTransfers(_ context.Context, _ string) ([]*types.Transfer, error) {
|
|
return []*types.Transfer{}, nil
|
|
}
|
|
|
|
func (p *Provider) CreateBankRelationship(_ context.Context, _, _, _, _, _ string) (*types.BankRelationship, error) {
|
|
return nil, fmt.Errorf("falcon: not applicable")
|
|
}
|
|
|
|
func (p *Provider) ListBankRelationships(_ context.Context, _ string) ([]*types.BankRelationship, error) {
|
|
return []*types.BankRelationship{}, nil
|
|
}
|
|
|
|
func (p *Provider) ListAssets(_ context.Context, _ string) ([]*types.Asset, error) {
|
|
pairs := []string{"BTC", "ETH", "SOL", "AVAX", "DOT", "LINK", "UNI", "AAVE", "MATIC", "ARB", "OP"}
|
|
assets := make([]*types.Asset, len(pairs))
|
|
for i, s := range pairs {
|
|
assets[i] = &types.Asset{
|
|
ID: s, Provider: "falcon", Symbol: s, Name: s + "/USD",
|
|
Class: "crypto", Status: "active", Tradable: true,
|
|
}
|
|
}
|
|
return assets, nil
|
|
}
|
|
|
|
func (p *Provider) GetAsset(_ context.Context, symbolOrID string) (*types.Asset, error) {
|
|
return &types.Asset{
|
|
ID: symbolOrID, Provider: "falcon", Symbol: symbolOrID,
|
|
Class: "crypto", Status: "active", Tradable: true,
|
|
}, nil
|
|
}
|
|
|
|
// --- Market Data ---
|
|
|
|
func (p *Provider) GetSnapshot(_ context.Context, _ string) (*types.MarketSnapshot, error) {
|
|
return nil, fmt.Errorf("falcon: market data not supported")
|
|
}
|
|
|
|
func (p *Provider) GetSnapshots(_ context.Context, _ []string) (map[string]*types.MarketSnapshot, error) {
|
|
return nil, fmt.Errorf("falcon: market data not supported")
|
|
}
|
|
|
|
func (p *Provider) GetBars(_ context.Context, _, _, _, _ string, _ int) ([]*types.Bar, error) {
|
|
return nil, fmt.Errorf("falcon: market data not supported")
|
|
}
|
|
|
|
func (p *Provider) GetLatestTrades(_ context.Context, _ []string) (map[string]*types.Trade, error) {
|
|
return nil, fmt.Errorf("falcon: market data not supported")
|
|
}
|
|
|
|
func (p *Provider) GetLatestQuotes(_ context.Context, _ []string) (map[string]*types.Quote, error) {
|
|
return nil, fmt.Errorf("falcon: market data not supported")
|
|
}
|
|
|
|
func (p *Provider) GetClock(_ context.Context) (*types.MarketClock, error) {
|
|
return nil, fmt.Errorf("falcon: clock not supported")
|
|
}
|
|
|
|
func (p *Provider) GetCalendar(_ context.Context, _, _ string) ([]*types.MarketCalendarDay, error) {
|
|
return nil, fmt.Errorf("falcon: calendar not supported")
|
|
}
|