service backend
This commit is contained in:
221
api/fx/oracle/internal/service/oracle/cross.go
Normal file
221
api/fx/oracle/internal/service/oracle/cross.go
Normal file
@@ -0,0 +1,221 @@
|
||||
package oracle
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/fx/storage/model"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
)
|
||||
|
||||
type priceSet struct {
|
||||
bid *big.Rat
|
||||
ask *big.Rat
|
||||
mid *big.Rat
|
||||
}
|
||||
|
||||
func (s *Service) computeCrossRate(ctx context.Context, pair *model.Pair, provider string) (*model.RateSnapshot, error) {
|
||||
if pair == nil || pair.Cross == nil || !pair.Cross.Enabled {
|
||||
return nil, merrors.ErrNoData
|
||||
}
|
||||
|
||||
baseSnap, err := s.fetchCrossLegSnapshot(ctx, pair.Cross.BaseLeg, provider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
quoteSnap, err := s.fetchCrossLegSnapshot(ctx, pair.Cross.QuoteLeg, provider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
basePrices, err := buildPriceSet(baseSnap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
quotePrices, err := buildPriceSet(quoteSnap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if pair.Cross.BaseLeg.Invert {
|
||||
basePrices, err = invertPriceSet(basePrices)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if pair.Cross.QuoteLeg.Invert {
|
||||
quotePrices, err = invertPriceSet(quotePrices)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
result := multiplyPriceSets(basePrices, quotePrices)
|
||||
if result.ask.Cmp(result.bid) < 0 {
|
||||
result.ask, result.bid = result.bid, result.ask
|
||||
}
|
||||
|
||||
spread := calcSpreadBps(result)
|
||||
|
||||
asOfMs := minNonZero(baseSnap.AsOfUnixMs, quoteSnap.AsOfUnixMs)
|
||||
if asOfMs == 0 {
|
||||
asOfMs = time.Now().UnixMilli()
|
||||
}
|
||||
asOf := time.UnixMilli(asOfMs)
|
||||
|
||||
rateRef := fmt.Sprintf("cross|%s/%s|%s|%s+%s", pair.Pair.Base, pair.Pair.Quote, provider, baseSnap.RateRef, quoteSnap.RateRef)
|
||||
|
||||
return &model.RateSnapshot{
|
||||
RateRef: rateRef,
|
||||
Pair: pair.Pair,
|
||||
Provider: provider,
|
||||
Mid: formatPrice(result.mid),
|
||||
Bid: formatPrice(result.bid),
|
||||
Ask: formatPrice(result.ask),
|
||||
SpreadBps: formatPrice(spread),
|
||||
AsOfUnixMs: asOfMs,
|
||||
AsOf: &asOf,
|
||||
Source: "cross_rate",
|
||||
ProviderRef: rateRef,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) fetchCrossLegSnapshot(ctx context.Context, leg model.CrossRateLeg, fallbackProvider string) (*model.RateSnapshot, error) {
|
||||
provider := fallbackProvider
|
||||
if strings.TrimSpace(leg.Provider) != "" {
|
||||
provider = leg.Provider
|
||||
}
|
||||
if provider == "" {
|
||||
return nil, merrors.InvalidArgument("oracle: cross leg provider missing")
|
||||
}
|
||||
return s.storage.Rates().LatestSnapshot(ctx, leg.Pair, provider)
|
||||
}
|
||||
|
||||
func buildPriceSet(rate *model.RateSnapshot) (priceSet, error) {
|
||||
if rate == nil {
|
||||
return priceSet{}, merrors.InvalidArgument("oracle: cross rate requires underlying snapshot")
|
||||
}
|
||||
ask, err := parsePrice(rate.Ask)
|
||||
if err != nil {
|
||||
return priceSet{}, err
|
||||
}
|
||||
bid, err := parsePrice(rate.Bid)
|
||||
if err != nil {
|
||||
return priceSet{}, err
|
||||
}
|
||||
mid, err := parsePrice(rate.Mid)
|
||||
if err != nil {
|
||||
return priceSet{}, err
|
||||
}
|
||||
|
||||
if ask == nil && bid == nil {
|
||||
if mid == nil {
|
||||
return priceSet{}, merrors.InvalidArgument("oracle: cross rate snapshot missing price data")
|
||||
}
|
||||
ask = new(big.Rat).Set(mid)
|
||||
bid = new(big.Rat).Set(mid)
|
||||
}
|
||||
if ask == nil && mid != nil {
|
||||
ask = new(big.Rat).Set(mid)
|
||||
}
|
||||
if bid == nil && mid != nil {
|
||||
bid = new(big.Rat).Set(mid)
|
||||
}
|
||||
if ask == nil || bid == nil {
|
||||
return priceSet{}, merrors.InvalidArgument("oracle: cross rate snapshot missing bid/ask data")
|
||||
}
|
||||
|
||||
ps := priceSet{
|
||||
bid: new(big.Rat).Set(bid),
|
||||
ask: new(big.Rat).Set(ask),
|
||||
mid: averageOrMid(bid, ask, mid),
|
||||
}
|
||||
if ps.ask.Cmp(ps.bid) < 0 {
|
||||
ps.ask, ps.bid = ps.bid, ps.ask
|
||||
}
|
||||
return ps, nil
|
||||
}
|
||||
|
||||
func parsePrice(value string) (*big.Rat, error) {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return nil, nil
|
||||
}
|
||||
return ratFromString(value)
|
||||
}
|
||||
|
||||
func averageOrMid(bid, ask, mid *big.Rat) *big.Rat {
|
||||
if mid != nil {
|
||||
return new(big.Rat).Set(mid)
|
||||
}
|
||||
sum := new(big.Rat).Add(bid, ask)
|
||||
return sum.Quo(sum, big.NewRat(2, 1))
|
||||
}
|
||||
|
||||
func invertPriceSet(ps priceSet) (priceSet, error) {
|
||||
if ps.ask.Sign() == 0 || ps.bid.Sign() == 0 {
|
||||
return priceSet{}, merrors.InvalidArgument("oracle: cannot invert zero price")
|
||||
}
|
||||
one := big.NewRat(1, 1)
|
||||
invBid := new(big.Rat).Quo(one, ps.ask)
|
||||
invAsk := new(big.Rat).Quo(one, ps.bid)
|
||||
var invMid *big.Rat
|
||||
if ps.mid != nil && ps.mid.Sign() != 0 {
|
||||
invMid = new(big.Rat).Quo(one, ps.mid)
|
||||
} else {
|
||||
invMid = averageOrMid(invBid, invAsk, nil)
|
||||
}
|
||||
result := priceSet{
|
||||
bid: invBid,
|
||||
ask: invAsk,
|
||||
mid: invMid,
|
||||
}
|
||||
if result.ask.Cmp(result.bid) < 0 {
|
||||
result.ask, result.bid = result.bid, result.ask
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func multiplyPriceSets(a, b priceSet) priceSet {
|
||||
result := priceSet{
|
||||
bid: mulRat(a.bid, b.bid),
|
||||
ask: mulRat(a.ask, b.ask),
|
||||
}
|
||||
result.mid = averageOrMid(result.bid, result.ask, nil)
|
||||
return result
|
||||
}
|
||||
|
||||
func calcSpreadBps(ps priceSet) *big.Rat {
|
||||
if ps.mid == nil || ps.mid.Sign() == 0 {
|
||||
return nil
|
||||
}
|
||||
spread := new(big.Rat).Sub(ps.ask, ps.bid)
|
||||
if spread.Sign() < 0 {
|
||||
spread.Neg(spread)
|
||||
}
|
||||
spread.Quo(spread, ps.mid)
|
||||
spread.Mul(spread, big.NewRat(10000, 1))
|
||||
return spread
|
||||
}
|
||||
|
||||
func minNonZero(values ...int64) int64 {
|
||||
var result int64
|
||||
for _, v := range values {
|
||||
if v <= 0 {
|
||||
continue
|
||||
}
|
||||
if result == 0 || v < result {
|
||||
result = v
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func formatPrice(r *big.Rat) string {
|
||||
if r == nil {
|
||||
return ""
|
||||
}
|
||||
return r.FloatString(8)
|
||||
}
|
||||
Reference in New Issue
Block a user