mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-13 15:09:42 +00:00
* Slight enhance of Coinbase tests Continual enhance of Coinbase tests The revamp continues Oh jeez the Orderbook part's unfinished don't look Coinbase revamp, Orderbook still unfinished * Coinbase revamp; CreateReport is still WIP * More coinbase improvements; onto sandbox testing * Coinbase revamp continues * Coinbase revamp continues * Coinbasepro revamp is ceaseless * Coinbase revamp, starting on advanced trade API * Coinbase Advanced Trade Starts in Ernest V3 done, onto V2 Coinbase revamp nears completion Coinbase revamp nears completion Test commit should fail Coinbase revamp nears completion * Coinbase revamp stage wrapper * Coinbase wrapper coherence continues * Coinbase wrapper continues writhing * Coinbase wrapper & codebase cleanup * Coinbase updates & wrap progress * More Coinbase wrapper progress * Wrapper is wrapped, kinda * Test & type checking * Coinbase REST revamp finished * Post-merge fix * WS revamp begins * WS Main Revamp Done? * CB websocket tidying up * Coinbase WS wrapperupperer * Coinbase revamp done?? * Linter progress * Continued lint cleanup * Further lint cleanup * Increased lint coverage * Does this fix all sloppy reassigns & shadowing? * Undoing retry policy change * Documentation regeneration * Coinbase code improvements * Providing warning about known issue * Updating an error to new format * Making gocritic happy * Review adherence * Endpoints moved to V3 & nil pointer fixes * Removing seemingly superfluous constant * Glorious improvements * Removing unused error * Partial public endpoint addition * Slight improvements * Wrapper improvements; still a few errors left in other packages * A lil Coinbase progress * Json cleaning * Lint appeasement * Config repair * Config fix (real) * Little fix * New public endpoint incorporation * Additional fixes * Improvements & Appeasements * LineSaver * Additional fixes * Another fix * Fixing picked nits * Quick fixies * Lil fixes * Subscriptions: Add List.Enabled * CoinbasePro: Add subscription templating * fixup! CoinbasePro: Add subscription templating * fixup! CoinbasePro: Add subscription templating * Comment fix * Subsequent fixes * Issues hopefully fixed * Lint fix * Glorious fixes * Json formatting * ShazNits * (L/N)i(n/)t * Adding a test * Tiny test improvement * Template patch testing * Fixes * Further shaznits * Lint nit * JWT move and other fixes * Small nits * Shaznit, singular * Post-merge fix * Post-merge fixes * Typo fix * Some glorious nits * Required changes * Stop going * Alias attempt * Alias fix & test cleanup * Test fix * GetDepositAddress logic improvement * Status update: Fixed * Lint fix * Happy birthday to PR 1480 * Cleanups * Necessary nit corrections * Fixing sillybug * As per request * Programming progress * Order fixes * Further fixies * Test fix * Pre-merge fixes * More shaznits * Context * Sonic error handling * Import fix * Better Sonic error handling * Perfect Sonic error handling? * F purge * Coinbase improvements * API Update Conformity * Coinbase continuation * Coinbase order improvements * Coinbase order improvements * CreateOrderConfig improvements * Managing API updates * Coinbase API update progression * jwt rename * Comment link fix * Coinbase v2 cleanup * Post-merge fixes * Review fixes * GK's suggestions * Linter fix * Minor gbjk fixes * Nit fixes * Merge fix * Lint fixes * Coinbase rename stage 1 * Coinbase rename stage 2 * Coinbase rename stage 3 * Coinbase rename stage 4 * Coinbase rename final fix * Coinbase: PoC on converting to request structs * Applying requested changes * Many review fixes, handled * Thrashed by nits * More minor modifications * The last nit!? --------- Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>
849 lines
26 KiB
Go
849 lines
26 KiB
Go
package funding
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/shopspring/decimal"
|
|
"github.com/thrasher-corp/gocryptotrader/backtester/common"
|
|
"github.com/thrasher-corp/gocryptotrader/backtester/data"
|
|
"github.com/thrasher-corp/gocryptotrader/backtester/data/kline"
|
|
"github.com/thrasher-corp/gocryptotrader/backtester/funding/trackingcurrencies"
|
|
gctcommon "github.com/thrasher-corp/gocryptotrader/common"
|
|
"github.com/thrasher-corp/gocryptotrader/currency"
|
|
"github.com/thrasher-corp/gocryptotrader/engine"
|
|
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/account"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/futures"
|
|
gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline"
|
|
gctorder "github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
|
"github.com/thrasher-corp/gocryptotrader/log"
|
|
)
|
|
|
|
// SetupFundingManager creates the funding holder. It carries knowledge about levels of funding
|
|
// across all execution handlers and enables fund transfers
|
|
func SetupFundingManager(exchManager *engine.ExchangeManager, usingExchangeLevelFunding, disableUSDTracking, verbose bool) (*FundManager, error) {
|
|
if exchManager == nil {
|
|
return nil, errExchangeManagerRequired
|
|
}
|
|
return &FundManager{
|
|
usingExchangeLevelFunding: usingExchangeLevelFunding,
|
|
disableUSDTracking: disableUSDTracking,
|
|
exchangeManager: exchManager,
|
|
verbose: verbose,
|
|
}, nil
|
|
}
|
|
|
|
// CreateFuturesCurrencyCode converts a currency pair into a code
|
|
// The main reasoning is that as a contract, it exists as an item even if
|
|
// it is formatted as BTC-1231. To treat it as a pair in the funding system
|
|
// would cause an increase in funds for BTC, when it is an increase in contracts
|
|
// This function is basic, but is important be explicit in why this is occurring
|
|
func CreateFuturesCurrencyCode(b, q currency.Code) currency.Code {
|
|
return currency.NewCode(fmt.Sprintf("%s-%s", b, q))
|
|
}
|
|
|
|
// CreateItem creates a new funding item
|
|
func CreateItem(exch string, a asset.Item, ci currency.Code, initialFunds, transferFee decimal.Decimal) (*Item, error) {
|
|
if initialFunds.IsNegative() {
|
|
return nil, fmt.Errorf("%v %v %v %w initial funds: %v", exch, a, ci, errNegativeAmountReceived, initialFunds)
|
|
}
|
|
if transferFee.IsNegative() {
|
|
return nil, fmt.Errorf("%v %v %v %w transfer fee: %v", exch, a, ci, errNegativeAmountReceived, transferFee)
|
|
}
|
|
|
|
return &Item{
|
|
exchange: strings.ToLower(exch),
|
|
asset: a,
|
|
currency: ci,
|
|
initialFunds: initialFunds,
|
|
available: initialFunds,
|
|
transferFee: transferFee,
|
|
snapshot: make(map[int64]ItemSnapshot),
|
|
}, nil
|
|
}
|
|
|
|
// LinkCollateralCurrency links an item to an existing currency code
|
|
// for collateral purposes
|
|
func (f *FundManager) LinkCollateralCurrency(item *Item, code currency.Code) error {
|
|
if item == nil {
|
|
return fmt.Errorf("%w missing item", gctcommon.ErrNilPointer)
|
|
}
|
|
if code.IsEmpty() {
|
|
return fmt.Errorf("%w unset currency", gctcommon.ErrNilPointer)
|
|
}
|
|
if !item.asset.IsFutures() {
|
|
return errNotFutures
|
|
}
|
|
if item.pairedWith != nil {
|
|
return fmt.Errorf("%w item already paired with %v", ErrAlreadyExists, item.pairedWith.currency)
|
|
}
|
|
|
|
for i := range f.items {
|
|
if f.items[i].currency.Equal(code) && f.items[i].asset == item.asset {
|
|
item.pairedWith = f.items[i]
|
|
return nil
|
|
}
|
|
}
|
|
collateral := &Item{
|
|
exchange: item.exchange,
|
|
asset: item.asset,
|
|
currency: code,
|
|
pairedWith: item,
|
|
isCollateral: true,
|
|
}
|
|
if err := f.AddItem(collateral); err != nil {
|
|
return err
|
|
}
|
|
item.pairedWith = collateral
|
|
return nil
|
|
}
|
|
|
|
// CreateSnapshot creates a Snapshot for an event's point in time
|
|
// as funding.snapshots is a map, it allows for the last event
|
|
// in the chronological list to establish the canon at X time
|
|
func (f *FundManager) CreateSnapshot(t time.Time) error {
|
|
if t.IsZero() {
|
|
return gctcommon.ErrDateUnset
|
|
}
|
|
for i := range f.items {
|
|
if f.items[i].snapshot == nil {
|
|
f.items[i].snapshot = make(map[int64]ItemSnapshot)
|
|
}
|
|
iss, ok := f.items[i].snapshot[t.UnixNano()]
|
|
if !ok {
|
|
iss = ItemSnapshot{
|
|
Time: t,
|
|
}
|
|
}
|
|
iss.Available = f.items[i].available
|
|
if !f.disableUSDTracking {
|
|
if f.items[i].trackingCandles == nil {
|
|
continue
|
|
}
|
|
var usdClosePrice decimal.Decimal
|
|
usdCandles, err := f.items[i].trackingCandles.GetStream()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for j := range usdCandles {
|
|
if usdCandles[j].GetTime().Equal(t) {
|
|
usdClosePrice = usdCandles[j].GetClosePrice()
|
|
break
|
|
}
|
|
}
|
|
iss.USDClosePrice = usdClosePrice
|
|
iss.USDValue = usdClosePrice.Mul(f.items[i].available)
|
|
}
|
|
|
|
f.items[i].snapshot[t.UnixNano()] = iss
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// AddUSDTrackingData adds USD tracking data to a funding item
|
|
// only in the event that it is not USD and there is data
|
|
func (f *FundManager) AddUSDTrackingData(k *kline.DataFromKline) error {
|
|
if f == nil || f.items == nil {
|
|
return gctcommon.ErrNilPointer
|
|
}
|
|
if f.disableUSDTracking {
|
|
return ErrUSDTrackingDisabled
|
|
}
|
|
baseSet := false
|
|
quoteSet := false
|
|
var basePairedWith currency.Code
|
|
for i := range f.items {
|
|
if baseSet && quoteSet {
|
|
return nil
|
|
}
|
|
if f.items[i].asset.IsFutures() && k.Item.Asset.IsFutures() {
|
|
if f.items[i].isCollateral {
|
|
err := f.setUSDCandles(k, f.items[i])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
f.items[i].trackingCandles = k
|
|
baseSet = true
|
|
}
|
|
continue
|
|
}
|
|
|
|
if strings.EqualFold(f.items[i].exchange, k.Item.Exchange) &&
|
|
f.items[i].asset == k.Item.Asset {
|
|
if f.items[i].currency.Equal(k.Item.Pair.Base) {
|
|
if trackingcurrencies.CurrencyIsUSDTracked(k.Item.Pair.Quote) {
|
|
f.items[i].trackingCandles = k
|
|
if f.items[i].pairedWith != nil {
|
|
basePairedWith = f.items[i].pairedWith.currency
|
|
}
|
|
}
|
|
baseSet = true
|
|
}
|
|
if trackingcurrencies.CurrencyIsUSDTracked(f.items[i].currency) {
|
|
if f.items[i].pairedWith != nil && !f.items[i].currency.Equal(basePairedWith) {
|
|
continue
|
|
}
|
|
err := f.setUSDCandles(k, f.items[i])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
quoteSet = true
|
|
}
|
|
}
|
|
}
|
|
if baseSet {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("%w %v %v %v", errCannotMatchTrackingToItem, k.Item.Exchange, k.Item.Asset, k.Item.Pair)
|
|
}
|
|
|
|
// setUSDCandles sets usd tracking candles
|
|
// usd stablecoins do not always match in value,
|
|
// this is a simplified implementation that can allow
|
|
// USD tracking for many currencies across many exchanges
|
|
func (f *FundManager) setUSDCandles(k *kline.DataFromKline, i *Item) error {
|
|
usdCandles := gctkline.Item{
|
|
Exchange: k.Item.Exchange,
|
|
Pair: currency.Pair{Delimiter: k.Item.Pair.Delimiter, Base: i.currency, Quote: currency.USD},
|
|
Asset: k.Item.Asset,
|
|
Interval: k.Item.Interval,
|
|
Candles: make([]gctkline.Candle, len(k.Item.Candles)),
|
|
}
|
|
for x := range usdCandles.Candles {
|
|
usdCandles.Candles[x] = gctkline.Candle{
|
|
Time: k.Item.Candles[x].Time,
|
|
Open: 1,
|
|
High: 1,
|
|
Low: 1,
|
|
Close: 1,
|
|
}
|
|
}
|
|
cpy := *k
|
|
cpy.Item = &usdCandles
|
|
cpy.Base = &data.Base{}
|
|
if err := cpy.Load(); err != nil {
|
|
return err
|
|
}
|
|
i.trackingCandles = &cpy
|
|
return nil
|
|
}
|
|
|
|
// CreatePair adds two funding items and associates them with one another
|
|
// the association allows for the same currency to be used multiple times when
|
|
// usingExchangeLevelFunding is false. eg BTC-USDT and LTC-USDT do not share the same
|
|
// USDT level funding
|
|
func CreatePair(base, quote *Item) (*SpotPair, error) {
|
|
if base == nil {
|
|
return nil, fmt.Errorf("base %w", gctcommon.ErrNilPointer)
|
|
}
|
|
if quote == nil {
|
|
return nil, fmt.Errorf("quote %w", gctcommon.ErrNilPointer)
|
|
}
|
|
// copy to prevent the off chance of sending in the same base OR quote
|
|
// to create a new pair with a new base OR quote
|
|
bCopy := *base
|
|
qCopy := *quote
|
|
bCopy.pairedWith = &qCopy
|
|
qCopy.pairedWith = &bCopy
|
|
return &SpotPair{base: &bCopy, quote: &qCopy}, nil
|
|
}
|
|
|
|
// CreateCollateral adds two funding items and associates them with one another
|
|
// the association allows for the same currency to be used multiple times when
|
|
// usingExchangeLevelFunding is false. eg BTC-USDT and LTC-USDT do not share the same
|
|
// USDT level funding
|
|
func CreateCollateral(contract, collateral *Item) (*CollateralPair, error) {
|
|
if contract == nil {
|
|
return nil, fmt.Errorf("base %w", gctcommon.ErrNilPointer)
|
|
}
|
|
if collateral == nil {
|
|
return nil, fmt.Errorf("quote %w", gctcommon.ErrNilPointer)
|
|
}
|
|
collateral.isCollateral = true
|
|
// copy to prevent the off chance of sending in the same base OR quote
|
|
// to create a new pair with a new base OR quote
|
|
bCopy := *contract
|
|
qCopy := *collateral
|
|
bCopy.pairedWith = &qCopy
|
|
qCopy.pairedWith = &bCopy
|
|
return &CollateralPair{contract: &bCopy, collateral: &qCopy}, nil
|
|
}
|
|
|
|
// Reset clears all settings
|
|
func (f *FundManager) Reset() error {
|
|
if f == nil {
|
|
return gctcommon.ErrNilPointer
|
|
}
|
|
*f = FundManager{}
|
|
return nil
|
|
}
|
|
|
|
// USDTrackingDisabled clears all settings
|
|
func (f *FundManager) USDTrackingDisabled() bool {
|
|
return f.disableUSDTracking
|
|
}
|
|
|
|
// GenerateReport builds report data for result HTML report
|
|
func (f *FundManager) GenerateReport() (*Report, error) {
|
|
report := Report{
|
|
UsingExchangeLevelFunding: f.usingExchangeLevelFunding,
|
|
DisableUSDTracking: f.disableUSDTracking,
|
|
}
|
|
items := make([]ReportItem, len(f.items))
|
|
for x := range f.items {
|
|
item := ReportItem{
|
|
Exchange: f.items[x].exchange,
|
|
Asset: f.items[x].asset,
|
|
Currency: f.items[x].currency,
|
|
InitialFunds: f.items[x].initialFunds,
|
|
TransferFee: f.items[x].transferFee,
|
|
FinalFunds: f.items[x].available,
|
|
IsCollateral: f.items[x].isCollateral,
|
|
AppendedViaAPI: f.items[x].appendedViaAPI,
|
|
}
|
|
|
|
if !f.disableUSDTracking &&
|
|
f.items[x].trackingCandles != nil {
|
|
usdStream, err := f.items[x].trackingCandles.GetStream()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
last, err := usdStream.Last()
|
|
if err != nil {
|
|
log.Errorf(common.FundManager, "USD tracking data is nil for %v %v %v, please ensure data is present", f.items[x].exchange, f.items[x].asset, f.items[x].currency)
|
|
}
|
|
first, err := usdStream.First()
|
|
if err != nil {
|
|
log.Errorf(common.FundManager, "USD tracking data is nil for %v %v %v, please ensure data is present", f.items[x].exchange, f.items[x].asset, f.items[x].currency)
|
|
}
|
|
if !item.IsCollateral {
|
|
item.USDInitialFunds = f.items[x].initialFunds.Mul(first.GetClosePrice())
|
|
item.USDFinalFunds = f.items[x].available.Mul(last.GetClosePrice())
|
|
}
|
|
|
|
item.USDInitialCostForOne = first.GetClosePrice()
|
|
item.USDFinalCostForOne = last.GetClosePrice()
|
|
item.USDPairCandle = f.items[x].trackingCandles
|
|
}
|
|
|
|
// create a breakdown of USD values and currency contributions over the span of run
|
|
var pricingOverTime []ItemSnapshot
|
|
snaps:
|
|
for _, snapshot := range f.items[x].snapshot {
|
|
pricingOverTime = append(pricingOverTime, snapshot)
|
|
if f.items[x].asset.IsFutures() || f.disableUSDTracking {
|
|
// futures contracts / collateral does not contribute to USD value
|
|
// no USD tracking means no USD values to breakdown
|
|
continue
|
|
}
|
|
for y := range report.USDTotalsOverTime {
|
|
if !report.USDTotalsOverTime[y].Time.Equal(snapshot.Time) {
|
|
continue
|
|
}
|
|
report.USDTotalsOverTime[y].USDValue = report.USDTotalsOverTime[y].USDValue.Add(snapshot.USDValue)
|
|
report.USDTotalsOverTime[y].Breakdown = append(report.USDTotalsOverTime[y].Breakdown, CurrencyContribution{
|
|
Currency: f.items[x].currency,
|
|
USDContribution: snapshot.USDValue,
|
|
})
|
|
continue snaps
|
|
}
|
|
report.USDTotalsOverTime = append(report.USDTotalsOverTime, ItemSnapshot{
|
|
Time: snapshot.Time,
|
|
USDValue: snapshot.USDValue,
|
|
Breakdown: []CurrencyContribution{
|
|
{
|
|
Currency: f.items[x].currency,
|
|
USDContribution: snapshot.USDValue,
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
sort.Slice(pricingOverTime, func(i, j int) bool {
|
|
return pricingOverTime[i].Time.Before(pricingOverTime[j].Time)
|
|
})
|
|
item.Snapshots = pricingOverTime
|
|
|
|
if f.items[x].initialFunds.IsZero() {
|
|
item.ShowInfinite = true
|
|
} else {
|
|
item.Difference = f.items[x].available.Sub(f.items[x].initialFunds).Div(f.items[x].initialFunds).Mul(decimal.NewFromInt(100))
|
|
}
|
|
if f.items[x].pairedWith != nil {
|
|
item.PairedWith = f.items[x].pairedWith.currency
|
|
}
|
|
report.InitialFunds = report.InitialFunds.Add(item.USDInitialFunds)
|
|
|
|
items[x] = item
|
|
}
|
|
|
|
if len(report.USDTotalsOverTime) > 0 {
|
|
sort.Slice(report.USDTotalsOverTime, func(i, j int) bool {
|
|
return report.USDTotalsOverTime[i].Time.Before(report.USDTotalsOverTime[j].Time)
|
|
})
|
|
report.FinalFunds = report.USDTotalsOverTime[len(report.USDTotalsOverTime)-1].USDValue
|
|
}
|
|
|
|
report.Items = items
|
|
return &report, nil
|
|
}
|
|
|
|
// Transfer allows transferring funds from one pretend exchange to another
|
|
func (f *FundManager) Transfer(amount decimal.Decimal, sender, receiver *Item, inclusiveFee bool) error {
|
|
if sender == nil || receiver == nil {
|
|
return gctcommon.ErrNilPointer
|
|
}
|
|
if amount.LessThanOrEqual(decimal.Zero) {
|
|
return errZeroAmountReceived
|
|
}
|
|
if inclusiveFee {
|
|
if sender.available.LessThan(amount) {
|
|
return fmt.Errorf("%w for %v", errNotEnoughFunds, sender.currency)
|
|
}
|
|
} else {
|
|
if sender.available.LessThan(amount.Add(sender.transferFee)) {
|
|
return fmt.Errorf("%w for %v", errNotEnoughFunds, sender.currency)
|
|
}
|
|
}
|
|
|
|
if !sender.currency.Equal(receiver.currency) {
|
|
return errTransferMustBeSameCurrency
|
|
}
|
|
if sender.currency.Equal(receiver.currency) &&
|
|
sender.exchange == receiver.exchange &&
|
|
sender.asset == receiver.asset {
|
|
return fmt.Errorf("%v %v %v %w", sender.exchange, sender.asset, sender.currency, errCannotTransferToSameFunds)
|
|
}
|
|
|
|
sendAmount := amount
|
|
receiveAmount := amount
|
|
if inclusiveFee {
|
|
receiveAmount = amount.Sub(sender.transferFee)
|
|
} else {
|
|
sendAmount = amount.Add(sender.transferFee)
|
|
}
|
|
err := sender.Reserve(sendAmount)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = receiver.IncreaseAvailable(receiveAmount)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return sender.Release(sendAmount, decimal.Zero)
|
|
}
|
|
|
|
// AddItem appends a new funding item. Will reject if exists by exchange asset currency
|
|
func (f *FundManager) AddItem(item *Item) error {
|
|
if f.Exists(item) {
|
|
return fmt.Errorf("cannot add item %v %v %v %w", item.exchange, item.asset, item.currency, ErrAlreadyExists)
|
|
}
|
|
f.items = append(f.items, item)
|
|
return nil
|
|
}
|
|
|
|
// Exists verifies whether there is a funding item that exists
|
|
// with the same exchange, asset and currency
|
|
func (f *FundManager) Exists(item *Item) bool {
|
|
for i := range f.items {
|
|
if f.items[i].Equal(item) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// AddPair adds a pair to the fund manager if it does not exist
|
|
func (f *FundManager) AddPair(p *SpotPair) error {
|
|
if f.Exists(p.base) {
|
|
return fmt.Errorf("%w %v", ErrAlreadyExists, p.base)
|
|
}
|
|
if f.Exists(p.quote) {
|
|
return fmt.Errorf("%w %v", ErrAlreadyExists, p.quote)
|
|
}
|
|
f.items = append(f.items, p.base, p.quote)
|
|
return nil
|
|
}
|
|
|
|
// IsUsingExchangeLevelFunding returns if using usingExchangeLevelFunding
|
|
func (f *FundManager) IsUsingExchangeLevelFunding() bool {
|
|
return f.usingExchangeLevelFunding
|
|
}
|
|
|
|
// GetFundingForEvent This will construct a funding based on a backtesting event
|
|
func (f *FundManager) GetFundingForEvent(ev common.Event) (IFundingPair, error) {
|
|
return f.getFundingForEAP(ev.GetExchange(), ev.GetAssetType(), ev.Pair())
|
|
}
|
|
|
|
// GetFundingForEAP This will construct a funding based on the exchange, asset, currency pair
|
|
func (f *FundManager) getFundingForEAP(exch string, a asset.Item, p currency.Pair) (IFundingPair, error) {
|
|
if a.IsFutures() {
|
|
var collat CollateralPair
|
|
for i := range f.items {
|
|
if f.items[i].MatchesCurrency(currency.NewCode(p.String())) {
|
|
collat.contract = f.items[i]
|
|
collat.collateral = f.items[i].pairedWith
|
|
return &collat, nil
|
|
}
|
|
}
|
|
} else {
|
|
var resp SpotPair
|
|
for i := range f.items {
|
|
if f.items[i].BasicEqual(exch, a, p.Base, p.Quote) {
|
|
resp.base = f.items[i]
|
|
continue
|
|
}
|
|
if f.items[i].BasicEqual(exch, a, p.Quote, p.Base) {
|
|
resp.quote = f.items[i]
|
|
}
|
|
}
|
|
if resp.base == nil {
|
|
return nil, fmt.Errorf("base %v %w", p.Base, ErrFundsNotFound)
|
|
}
|
|
if resp.quote == nil {
|
|
return nil, fmt.Errorf("quote %v %w", p.Quote, ErrFundsNotFound)
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
return nil, fmt.Errorf("%v %v %v %w", exch, a, p, ErrFundsNotFound)
|
|
}
|
|
|
|
// Liquidate will remove all funding for all items belonging to an exchange
|
|
func (f *FundManager) Liquidate(ev common.Event) error {
|
|
if ev == nil {
|
|
return fmt.Errorf("%w event", gctcommon.ErrNilPointer)
|
|
}
|
|
for i := range f.items {
|
|
if f.items[i].exchange == ev.GetExchange() {
|
|
f.items[i].reserved = decimal.Zero
|
|
f.items[i].available = decimal.Zero
|
|
f.items[i].isLiquidated = true
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetAllFunding returns basic representations of all current
|
|
// holdings from the latest point
|
|
func (f *FundManager) GetAllFunding() ([]BasicItem, error) {
|
|
result := make([]BasicItem, len(f.items))
|
|
for i := range f.items {
|
|
var usd decimal.Decimal
|
|
if f.items[i].trackingCandles != nil {
|
|
latest, err := f.items[i].trackingCandles.Latest()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if latest != nil {
|
|
usd = latest.GetClosePrice()
|
|
}
|
|
}
|
|
result[i] = BasicItem{
|
|
Exchange: f.items[i].exchange,
|
|
Asset: f.items[i].asset,
|
|
Currency: f.items[i].currency,
|
|
InitialFunds: f.items[i].initialFunds,
|
|
Available: f.items[i].available,
|
|
Reserved: f.items[i].reserved,
|
|
USDPrice: usd,
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// UpdateFundingFromLiveData forcefully updates funding from a live source
|
|
func (f *FundManager) UpdateFundingFromLiveData(initialFundsSet bool) error {
|
|
exchanges, err := f.exchangeManager.GetExchanges()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for x := range exchanges {
|
|
var creds *account.Credentials
|
|
creds, err = exchanges[x].GetCredentials(context.TODO())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
assets := exchanges[x].GetAssetTypes(false)
|
|
for y := range assets {
|
|
if assets[y].IsFutures() {
|
|
// we set all holdings as spot
|
|
// futures currency holdings are collateral in the collateral currency
|
|
continue
|
|
}
|
|
var acc account.Holdings
|
|
acc, err = exchanges[x].UpdateAccountInfo(context.TODO(), assets[y])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for z := range acc.Accounts {
|
|
if !acc.Accounts[z].Credentials.Equal(creds) {
|
|
continue
|
|
}
|
|
for i := range acc.Accounts[z].Currencies {
|
|
err = f.SetFunding(exchanges[x].GetName(), assets[y], &acc.Accounts[z].Currencies[i], initialFundsSet)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// UpdateAllCollateral will update the collateral values
|
|
// of all stored exchanges
|
|
func (f *FundManager) UpdateAllCollateral(isLive, initialFundsSet bool) error {
|
|
exchanges, err := f.exchangeManager.GetExchanges()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for x := range exchanges {
|
|
exchName := strings.ToLower(exchanges[x].GetName())
|
|
exchangeCollateralCalculator := &futures.TotalCollateralCalculator{
|
|
CalculateOffline: !isLive,
|
|
}
|
|
for y := range f.items {
|
|
if f.items[y].exchange != exchName {
|
|
continue
|
|
}
|
|
if f.items[y].asset.IsFutures() {
|
|
// futures positions aren't collateral, they utilise it
|
|
continue
|
|
}
|
|
var usd decimal.Decimal
|
|
if f.items[y].trackingCandles != nil {
|
|
var latest data.Event
|
|
latest, err = f.items[y].trackingCandles.Latest()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if latest != nil {
|
|
usd = latest.GetClosePrice()
|
|
}
|
|
}
|
|
if usd.IsZero() && exchangeCollateralCalculator.CalculateOffline {
|
|
continue
|
|
}
|
|
side := gctorder.Buy
|
|
if !f.items[y].available.GreaterThan(decimal.Zero) {
|
|
side = gctorder.Sell
|
|
}
|
|
|
|
exchangeCollateralCalculator.CollateralAssets = append(exchangeCollateralCalculator.CollateralAssets, futures.CollateralCalculator{
|
|
CalculateOffline: !isLive,
|
|
CollateralCurrency: f.items[y].currency,
|
|
Asset: f.items[y].asset,
|
|
Side: side,
|
|
FreeCollateral: f.items[y].available,
|
|
LockedCollateral: f.items[y].reserved,
|
|
USDPrice: usd,
|
|
})
|
|
}
|
|
|
|
var collateral *futures.TotalCollateralResponse
|
|
collateral, err = exchanges[x].CalculateTotalCollateral(context.TODO(), exchangeCollateralCalculator)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for y := range f.items {
|
|
if f.items[y].exchange == exchName &&
|
|
f.items[y].isCollateral {
|
|
if f.verbose {
|
|
log.Infof(common.FundManager, "Setting collateral %v %v %v to %v", f.items[y].exchange, f.items[y].asset, f.items[y].currency, collateral.AvailableCollateral)
|
|
}
|
|
f.items[y].available = collateral.AvailableCollateral
|
|
if !initialFundsSet {
|
|
f.items[y].initialFunds = collateral.AvailableCollateral
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// UpdateCollateralForEvent will recalculate collateral for an exchange
|
|
// based on the event passed in
|
|
func (f *FundManager) UpdateCollateralForEvent(ev common.Event, isLive bool) error {
|
|
if ev == nil {
|
|
return common.ErrNilEvent
|
|
}
|
|
if !f.HasFutures() {
|
|
// no collateral, no need to update
|
|
return nil
|
|
}
|
|
|
|
exchMap := make(map[string]exchange.IBotExchange)
|
|
var collateralAmount decimal.Decimal
|
|
var err error
|
|
calculator := futures.TotalCollateralCalculator{
|
|
CalculateOffline: !isLive,
|
|
}
|
|
|
|
for i := range f.items {
|
|
if f.items[i].asset.IsFutures() {
|
|
// futures positions aren't collateral, they utilise it
|
|
continue
|
|
}
|
|
_, ok := exchMap[f.items[i].exchange]
|
|
if !ok {
|
|
var exch exchange.IBotExchange
|
|
exch, err = f.exchangeManager.GetExchangeByName(f.items[i].exchange)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
exchMap[f.items[i].exchange] = exch
|
|
}
|
|
var usd decimal.Decimal
|
|
if f.items[i].trackingCandles != nil {
|
|
var latest data.Event
|
|
latest, err = f.items[i].trackingCandles.Latest()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if latest != nil {
|
|
usd = latest.GetClosePrice()
|
|
}
|
|
}
|
|
if usd.IsZero() {
|
|
continue
|
|
}
|
|
side := gctorder.Buy
|
|
if !f.items[i].available.GreaterThan(decimal.Zero) {
|
|
side = gctorder.Sell
|
|
}
|
|
|
|
calculator.CollateralAssets = append(calculator.CollateralAssets, futures.CollateralCalculator{
|
|
CalculateOffline: !isLive,
|
|
CollateralCurrency: f.items[i].currency,
|
|
Asset: f.items[i].asset,
|
|
Side: side,
|
|
FreeCollateral: f.items[i].available,
|
|
LockedCollateral: f.items[i].reserved,
|
|
USDPrice: usd,
|
|
})
|
|
}
|
|
exch, ok := exchMap[ev.GetExchange()]
|
|
if !ok {
|
|
return fmt.Errorf("%v %w", ev.GetExchange(), engine.ErrExchangeNotFound)
|
|
}
|
|
futureCurrency, futureAsset, err := exch.GetCollateralCurrencyForContract(ev.GetAssetType(), ev.Pair())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
collat, err := exchMap[ev.GetExchange()].CalculateTotalCollateral(context.TODO(), &calculator)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for i := range f.items {
|
|
if f.items[i].exchange == ev.GetExchange() &&
|
|
f.items[i].asset == futureAsset &&
|
|
f.items[i].currency.Equal(futureCurrency) {
|
|
if f.verbose {
|
|
log.Infof(common.FundManager, "Setting collateral %v %v %v to %v", f.items[i].exchange, f.items[i].asset, f.items[i].currency, collat.AvailableCollateral)
|
|
}
|
|
f.items[i].available = collat.AvailableCollateral
|
|
return nil
|
|
}
|
|
}
|
|
return fmt.Errorf("%w to allocate %v to %v %v %v", ErrFundsNotFound, collateralAmount, ev.GetExchange(), ev.GetAssetType(), futureCurrency)
|
|
}
|
|
|
|
// HasFutures returns whether the funding manager contains any futures assets
|
|
func (f *FundManager) HasFutures() bool {
|
|
for i := range f.items {
|
|
if f.items[i].isCollateral || f.items[i].asset.IsFutures() {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// RealisePNL adds the realised PNL to a receiving exchange asset pair
|
|
func (f *FundManager) RealisePNL(receivingExchange string, receivingAsset asset.Item, receivingCurrency currency.Code, realisedPNL decimal.Decimal) error {
|
|
for i := range f.items {
|
|
if f.items[i].exchange == receivingExchange &&
|
|
f.items[i].asset == receivingAsset &&
|
|
f.items[i].currency.Equal(receivingCurrency) {
|
|
return f.items[i].TakeProfit(realisedPNL)
|
|
}
|
|
}
|
|
return fmt.Errorf("%w to allocate %v to %v %v %v", ErrFundsNotFound, realisedPNL, receivingExchange, receivingAsset, receivingCurrency)
|
|
}
|
|
|
|
// HasExchangeBeenLiquidated checks for any items with a matching exchange
|
|
// and returns whether it has been liquidated
|
|
func (f *FundManager) HasExchangeBeenLiquidated(ev common.Event) bool {
|
|
for i := range f.items {
|
|
if ev.GetExchange() == f.items[i].exchange {
|
|
return f.items[i].isLiquidated
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// SetFunding overwrites a funding setting. This is for live trading
|
|
// where external wallet amounts need to be synced
|
|
// As external sources may have additional currencies and balances
|
|
// versus the strategy currencies, they must be appended to
|
|
// help calculate collateral
|
|
func (f *FundManager) SetFunding(exchName string, item asset.Item, balance *account.Balance, initialFundsSet bool) error {
|
|
if exchName == "" {
|
|
return gctcommon.ErrExchangeNameNotSet
|
|
}
|
|
if !item.IsValid() {
|
|
return asset.ErrNotSupported
|
|
}
|
|
if balance == nil {
|
|
return gctcommon.ErrNilPointer
|
|
}
|
|
if balance.Currency.IsEmpty() {
|
|
return currency.ErrCurrencyCodeEmpty
|
|
}
|
|
|
|
exchName = strings.ToLower(exchName)
|
|
amount := decimal.NewFromFloat(balance.Total)
|
|
for i := range f.items {
|
|
if f.items[i].asset.IsFutures() {
|
|
continue
|
|
}
|
|
if f.items[i].exchange != exchName ||
|
|
f.items[i].asset != item ||
|
|
!f.items[i].currency.Equal(balance.Currency) {
|
|
continue
|
|
}
|
|
if f.verbose {
|
|
log.Infof(common.FundManager, "Setting %v %v %v balance to %v", exchName, item, balance.Currency, balance.Total)
|
|
}
|
|
if !initialFundsSet {
|
|
f.items[i].initialFunds = amount
|
|
}
|
|
f.items[i].available = amount
|
|
return nil
|
|
}
|
|
if f.verbose {
|
|
log.Debugf(common.FundManager, "Appending balance %v %v %v to %v", exchName, item, balance.Currency, balance.Total)
|
|
}
|
|
f.items = append(f.items, &Item{
|
|
exchange: exchName,
|
|
asset: item,
|
|
currency: balance.Currency,
|
|
initialFunds: amount,
|
|
available: amount,
|
|
appendedViaAPI: true,
|
|
})
|
|
return nil
|
|
}
|