Files
gocryptotrader/exchanges/account/account.go
Scott 017cdf1384 Backtester: Live trading upgrades (#1023)
* Modifications for a smoother live run

* Fixes data appending

* Successfully allows multi-currency live trading. Adds multiple currencies to live DCA strategy

* Attempting to get cash and carry working

* Poor attempts at sorting out data and appending it properly with USD in mind

* =designs new live data handler

* Updates cash and carry strat to work

* adds test coverage. begins closeallpositions function

* Updates cash and carry to work live

* New kline.Event type. Cancels orders on close. Rn types

* =Fixes USD funding issue

* =fixes tests

* fixes tests AGAIN

* adds coverage to close all orders

* crummy tests, should override

* more tests

* more tests

* more coverage

* removes scourge of currency.Pair maps. More tests

* missed currency stuff

* Fixes USD data issue & collateral issue. Needs to close ALL orders

* Now triggers updates on the very first data entry

* All my problems are solved now????

* fixes tests, extends coverage

* there is some really funky candle stuff going on

* my brain is melting

* better shutdown management, fixes freezing bug

* fixes data duplication issues, adds retries to requests

* reduces logging, adds verbose options

* expands coverage over all new functionality

* fixes fun bug from curr == curr to curr.Equal(curr)

* fixes setup issues and tests

* starts adding external wallet amounts for funding

* more setup for assets

* setup live fund calcs and placing orders

* successfully performs automated cash and carry

* merge fixes

* funding properly set at all times

* fixes some bugs, need to address currencystatistics still

* adds 'appeneded' trait, attempts to fix some stats

* fixes stat bugs, adds cool new fetchfees feature

* fixes terrible processing bugs

* tightens realorder stats, sadly loses some live stats

* this actually sets everything correctly for bothcd ..cd ..cd ..cd ..cd ..!

* fix tests

* coverage

* beautiful new test coverage

* docs

* adds new fee getter delayer

* commits from the correct directory

* Lint

* adds verbose to fund manager

* Fix bug in t2b2 strat. Update dca live config. Docs

* go mod tidy

* update buf

* buf + test improvement

* Post merge fixes

* fixes surprise offset bug

* fix sizing restrictions for cash and carry

* fix server lints

* merge fixes

* test fixesss

* lintle fixles

* slowloris

* rn run to task, bug fixes, close all on close

* rpc lint and fixes

* bugfix: order manager not processing orders properly

* somewhat addresses nits

* absolutely broken end of day commit

* absolutely massive knockon effects from nits

* massive knockon effects continue

* fixes things

* address remaining nits

* jk now fixes things

* addresses the easier nits

* more nit fixers

* more niterinos addressederinos

* refactors holdings and does some nits

* so buf

* addresses some nits, fixes holdings bugs

* cleanup

* attempts to fix alert chans to prevent many chans waiting?

* terrible code, will revert

* to be reviewed in detail tomorrow

* Fixes up channel system

* smashes those nits

* fixes extra candles, fixes collateral bug, tests

* fixes data races, introduces reflection

* more checks n tests

* Fixes cash and carry issues. Fixes more cool bugs

* fixes ~typer~ typo

* replace spot strats from ftx to binance

* fixes all the tests I just destroyed

* removes example path, rm verbose

* 1) what 2) removes FTX references from the Backtester

* renamed, non-working strategies

* Removes FTX references almost as fast as sbf removes funds

* regen docs, add contrib names,sort contrib names

* fixes merge renamings

* Addresses nits. Fixes setting API credentials. Fixes Binance limit retrieval

* Fixes live order bugs with real orders and without

* Apply suggestions from code review

Co-authored-by: Adrian Gallagher <adrian.gallagher@thrasher.io>

* Update backtester/engine/live.go

Co-authored-by: Adrian Gallagher <adrian.gallagher@thrasher.io>

* Update backtester/engine/live.go

Co-authored-by: Adrian Gallagher <adrian.gallagher@thrasher.io>

* Update backtester/config/strategyconfigbuilder/main.go

Co-authored-by: Adrian Gallagher <adrian.gallagher@thrasher.io>

* updates docs

* even better docs

Co-authored-by: Adrian Gallagher <adrian.gallagher@thrasher.io>
2023-01-05 13:03:17 +11:00

355 lines
10 KiB
Go

package account
import (
"errors"
"fmt"
"strings"
"time"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/dispatch"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
)
func init() {
service.exchangeAccounts = make(map[string]*Accounts)
service.mux = dispatch.GetNewMux(nil)
}
var (
errHoldingsIsNil = errors.New("holdings cannot be nil")
errExchangeNameUnset = errors.New("exchange name unset")
errExchangeHoldingsNotFound = errors.New("exchange holdings not found")
errAssetHoldingsNotFound = errors.New("asset holdings not found")
errExchangeAccountsNotFound = errors.New("exchange accounts not found")
errNoExchangeSubAccountBalances = errors.New("no exchange sub account balances")
errNoBalanceFound = errors.New("no balance found")
errBalanceIsNil = errors.New("balance is nil")
errNoCredentialBalances = errors.New("no balances associated with credentials")
errCredentialsAreNil = errors.New("credentials are nil")
)
// CollectBalances converts a map of sub-account balances into a slice
func CollectBalances(accountBalances map[string][]Balance, assetType asset.Item) (accounts []SubAccount, err error) {
if accountBalances == nil {
return nil, errAccountBalancesIsNil
}
if !assetType.IsValid() {
return nil, fmt.Errorf("%s, %w", assetType, asset.ErrNotSupported)
}
accounts = make([]SubAccount, 0, len(accountBalances))
for accountID, balances := range accountBalances {
accounts = append(accounts, SubAccount{
ID: accountID,
AssetType: assetType,
Currencies: balances,
})
}
return
}
// SubscribeToExchangeAccount subscribes to your exchange account
func SubscribeToExchangeAccount(exchange string) (dispatch.Pipe, error) {
exchange = strings.ToLower(exchange)
service.mu.Lock()
defer service.mu.Unlock()
accounts, ok := service.exchangeAccounts[exchange]
if !ok {
return dispatch.Pipe{}, fmt.Errorf("cannot subscribe %s %w",
exchange,
errExchangeAccountsNotFound)
}
return service.mux.Subscribe(accounts.ID)
}
// Process processes new account holdings updates
func Process(h *Holdings, c *Credentials) error {
return service.Update(h, c)
}
// GetHoldings returns full holdings for an exchange.
// NOTE: Due to credentials these amounts could be N*APIKEY actual holdings.
// TODO: Add jurisdiction and differentiation between APIKEY holdings.
func GetHoldings(exch string, creds *Credentials, assetType asset.Item) (Holdings, error) {
if exch == "" {
return Holdings{}, errExchangeNameUnset
}
if creds.IsEmpty() {
return Holdings{}, fmt.Errorf("%s %s %w", exch, assetType, errCredentialsAreNil)
}
if !assetType.IsValid() {
return Holdings{}, fmt.Errorf("%s %s %w", exch, assetType, asset.ErrNotSupported)
}
exch = strings.ToLower(exch)
service.mu.Lock()
defer service.mu.Unlock()
accounts, ok := service.exchangeAccounts[exch]
if !ok {
return Holdings{}, fmt.Errorf("%s %s %w", exch, assetType, errExchangeHoldingsNotFound)
}
var accountsHoldings []SubAccount
subAccountHoldings, ok := accounts.SubAccounts[*creds]
if !ok {
return Holdings{}, fmt.Errorf("%s %s %s %w",
exch,
creds,
assetType,
errNoCredentialBalances)
}
for subAccount, assetHoldings := range subAccountHoldings {
for ai, currencyHoldings := range assetHoldings {
if ai != assetType {
continue
}
var currencyBalances = make([]Balance, len(currencyHoldings))
target := 0
for item, balance := range currencyHoldings {
balance.m.Lock()
currencyBalances[target] = Balance{
Currency: currency.Code{Item: item, UpperCase: true},
Total: balance.total,
Hold: balance.hold,
Free: balance.free,
AvailableWithoutBorrow: balance.availableWithoutBorrow,
Borrowed: balance.borrowed,
}
balance.m.Unlock()
target++
}
if len(currencyBalances) == 0 {
continue
}
cpy := *creds
if cpy.SubAccount == "" {
cpy.SubAccount = subAccount
}
accountsHoldings = append(accountsHoldings, SubAccount{
Credentials: Protected{creds: cpy},
ID: subAccount,
AssetType: ai,
Currencies: currencyBalances,
})
break
}
}
if len(accountsHoldings) == 0 {
return Holdings{}, fmt.Errorf("%s %s %w",
exch,
assetType,
errAssetHoldingsNotFound)
}
return Holdings{Exchange: exch, Accounts: accountsHoldings}, nil
}
// GetBalance returns the internal balance for that asset item.
func GetBalance(exch, subAccount string, creds *Credentials, ai asset.Item, c currency.Code) (*ProtectedBalance, error) {
if exch == "" {
return nil, fmt.Errorf("cannot get balance: %w", errExchangeNameUnset)
}
if !ai.IsValid() {
return nil, fmt.Errorf("cannot get balance: %s %w", ai, asset.ErrNotSupported)
}
if creds.IsEmpty() {
return nil, fmt.Errorf("cannot get balance: %w", errCredentialsAreNil)
}
if c.IsEmpty() {
return nil, fmt.Errorf("cannot get balance: %w", currency.ErrCurrencyCodeEmpty)
}
exch = strings.ToLower(exch)
service.mu.Lock()
defer service.mu.Unlock()
accounts, ok := service.exchangeAccounts[exch]
if !ok {
return nil, fmt.Errorf("%s %w", exch, errExchangeHoldingsNotFound)
}
subAccounts, ok := accounts.SubAccounts[*creds]
if !ok {
return nil, fmt.Errorf("%s %s %w",
exch, creds, errNoCredentialBalances)
}
assetBalances, ok := subAccounts[subAccount]
if !ok {
return nil, fmt.Errorf("%s %s %w",
exch, subAccount, errNoExchangeSubAccountBalances)
}
currencyBalances, ok := assetBalances[ai]
if !ok {
return nil, fmt.Errorf("%s %s %s %w",
exch, subAccount, ai, errAssetHoldingsNotFound)
}
bal, ok := currencyBalances[c.Item]
if !ok {
return nil, fmt.Errorf("%s %s %s %s %w",
exch, subAccount, ai, c, errNoBalanceFound)
}
return bal, nil
}
// Update updates holdings with new account info
func (s *Service) Update(incoming *Holdings, creds *Credentials) error {
if incoming == nil {
return fmt.Errorf("cannot update holdings: %w", errHoldingsIsNil)
}
if incoming.Exchange == "" {
return fmt.Errorf("cannot update holdings: %w", errExchangeNameUnset)
}
if creds.IsEmpty() {
return fmt.Errorf("cannot update holdings: %w", errCredentialsAreNil)
}
exch := strings.ToLower(incoming.Exchange)
s.mu.Lock()
defer s.mu.Unlock()
accounts, ok := s.exchangeAccounts[exch]
if !ok {
id, err := s.mux.GetID()
if err != nil {
return err
}
accounts = &Accounts{
ID: id,
SubAccounts: make(map[Credentials]map[string]map[asset.Item]map[*currency.Item]*ProtectedBalance),
}
s.exchangeAccounts[exch] = accounts
}
var errs common.Errors
for x := range incoming.Accounts {
if !incoming.Accounts[x].AssetType.IsValid() {
errs = append(errs, fmt.Errorf("cannot load sub account holdings for %s [%s] %w",
incoming.Accounts[x].ID,
incoming.Accounts[x].AssetType,
asset.ErrNotSupported))
continue
}
// This assignment outside of scope is designed to have minimal impact
// on the exchange implementation UpdateAccountInfo() and portfoio
// management.
// TODO: Update incoming Holdings type to already be populated. (Suggestion)
cpy := *creds
if cpy.SubAccount == "" {
cpy.SubAccount = incoming.Accounts[x].ID
}
incoming.Accounts[x].Credentials.creds = cpy
var subAccounts map[string]map[asset.Item]map[*currency.Item]*ProtectedBalance
subAccounts, ok = accounts.SubAccounts[*creds]
if !ok {
subAccounts = make(map[string]map[asset.Item]map[*currency.Item]*ProtectedBalance)
accounts.SubAccounts[*creds] = subAccounts
}
var accountAssets map[asset.Item]map[*currency.Item]*ProtectedBalance
accountAssets, ok = subAccounts[incoming.Accounts[x].ID]
if !ok {
accountAssets = make(map[asset.Item]map[*currency.Item]*ProtectedBalance)
// Note: Sub accounts are case sensitive and an account "name" is
// different to account "naMe".
subAccounts[incoming.Accounts[x].ID] = accountAssets
}
var currencyBalances map[*currency.Item]*ProtectedBalance
currencyBalances, ok = accountAssets[incoming.Accounts[x].AssetType]
if !ok {
currencyBalances = make(map[*currency.Item]*ProtectedBalance)
accountAssets[incoming.Accounts[x].AssetType] = currencyBalances
}
for y := range incoming.Accounts[x].Currencies {
bal := currencyBalances[incoming.Accounts[x].Currencies[y].Currency.Item]
if bal == nil {
bal = &ProtectedBalance{}
currencyBalances[incoming.Accounts[x].Currencies[y].Currency.Item] = bal
}
bal.load(incoming.Accounts[x].Currencies[y])
}
}
err := s.mux.Publish(incoming, accounts.ID)
if err != nil {
return err
}
if errs != nil {
return errs
}
return nil
}
// load checks to see if there is a change from incoming balance, if there is a
// change it will change then alert external routines.
func (b *ProtectedBalance) load(change Balance) {
b.m.Lock()
defer b.m.Unlock()
if b.total == change.Total &&
b.hold == change.Hold &&
b.free == change.Free &&
b.availableWithoutBorrow == change.AvailableWithoutBorrow &&
b.borrowed == change.Borrowed {
return
}
b.total = change.Total
b.hold = change.Hold
b.free = change.Free
b.availableWithoutBorrow = change.AvailableWithoutBorrow
b.borrowed = change.Borrowed
b.notice.Alert()
}
// Wait waits for a change in amounts for an asset type. This will pause
// indefinitely if no change ever occurs. Max wait will return true if it failed
// to achieve a state change in the time specified. If Max wait is not specified
// it will default to a minute wait time.
func (b *ProtectedBalance) Wait(maxWait time.Duration) (wait <-chan bool, cancel chan<- struct{}, err error) {
if b == nil {
return nil, nil, errBalanceIsNil
}
if maxWait <= 0 {
maxWait = time.Minute
}
ch := make(chan struct{})
go func(ch chan<- struct{}, until time.Duration) {
time.Sleep(until)
close(ch)
}(ch, maxWait)
return b.notice.Wait(ch), ch, nil
}
// GetFree returns the current free balance for the exchange
func (b *ProtectedBalance) GetFree() float64 {
if b == nil {
return 0
}
b.m.Lock()
defer b.m.Unlock()
return b.free
}