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) }