mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-06-08 23:16:54 +00:00
* 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>
287 lines
11 KiB
Go
287 lines
11 KiB
Go
package statistics
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
|
|
"github.com/shopspring/decimal"
|
|
"github.com/thrasher-corp/gocryptotrader/backtester/funding"
|
|
gctcommon "github.com/thrasher-corp/gocryptotrader/common"
|
|
gctmath "github.com/thrasher-corp/gocryptotrader/common/math"
|
|
"github.com/thrasher-corp/gocryptotrader/currency"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
|
gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline"
|
|
)
|
|
|
|
// CalculateFundingStatistics calculates funding statistics for total USD strategy results
|
|
// along with individual funding item statistics
|
|
func CalculateFundingStatistics(funds funding.IFundingManager, currStats map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*CurrencyPairStatistic, riskFreeRate decimal.Decimal, interval gctkline.Interval) (*FundingStatistics, error) {
|
|
if currStats == nil {
|
|
return nil, gctcommon.ErrNilPointer
|
|
}
|
|
report, err := funds.GenerateReport()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if report == nil {
|
|
return nil, errReceivedNoData
|
|
}
|
|
response := &FundingStatistics{
|
|
Report: report,
|
|
}
|
|
for i := range report.Items {
|
|
exchangeAssetStats, ok := currStats[report.Items[i].Exchange][report.Items[i].Asset]
|
|
if !ok {
|
|
if report.Items[i].AppendedViaAPI {
|
|
// items added via API may not have been processed along with typical events
|
|
// are not relevant to calculating statistics
|
|
continue
|
|
}
|
|
return nil, fmt.Errorf("%w for %v %v",
|
|
errNoRelevantStatsFound,
|
|
report.Items[i].Exchange,
|
|
report.Items[i].Asset)
|
|
}
|
|
var relevantStats []relatedCurrencyPairStatistics
|
|
for b, baseMap := range exchangeAssetStats {
|
|
for q, v := range baseMap {
|
|
if b.Currency().Equal(report.Items[i].Currency) {
|
|
relevantStats = append(relevantStats, relatedCurrencyPairStatistics{isBaseCurrency: true, stat: v})
|
|
continue
|
|
}
|
|
if q.Currency().Equal(report.Items[i].Currency) {
|
|
relevantStats = append(relevantStats, relatedCurrencyPairStatistics{stat: v})
|
|
}
|
|
}
|
|
}
|
|
var fundingStat *FundingItemStatistics
|
|
fundingStat, err = CalculateIndividualFundingStatistics(report.DisableUSDTracking, &report.Items[i], relevantStats)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
response.Items = append(response.Items, *fundingStat)
|
|
}
|
|
if report.DisableUSDTracking {
|
|
return response, nil
|
|
}
|
|
usdStats := &TotalFundingStatistics{
|
|
HighestHoldingValue: ValueAtTime{},
|
|
LowestHoldingValue: ValueAtTime{},
|
|
RiskFreeRate: riskFreeRate,
|
|
}
|
|
|
|
for i := range report.USDTotalsOverTime {
|
|
if usdStats.HighestHoldingValue.Value.LessThan(report.USDTotalsOverTime[i].USDValue) {
|
|
usdStats.HighestHoldingValue.Time = report.USDTotalsOverTime[i].Time
|
|
usdStats.HighestHoldingValue.Value = report.USDTotalsOverTime[i].USDValue
|
|
}
|
|
if usdStats.LowestHoldingValue.Value.IsZero() {
|
|
usdStats.LowestHoldingValue.Time = report.USDTotalsOverTime[i].Time
|
|
usdStats.LowestHoldingValue.Value = report.USDTotalsOverTime[i].USDValue
|
|
}
|
|
if usdStats.LowestHoldingValue.Value.GreaterThan(report.USDTotalsOverTime[i].USDValue) && !usdStats.LowestHoldingValue.Value.IsZero() {
|
|
usdStats.LowestHoldingValue.Time = report.USDTotalsOverTime[i].Time
|
|
usdStats.LowestHoldingValue.Value = report.USDTotalsOverTime[i].USDValue
|
|
}
|
|
usdStats.HoldingValues = append(usdStats.HoldingValues, ValueAtTime{Time: report.USDTotalsOverTime[i].Time, Value: report.USDTotalsOverTime[i].USDValue})
|
|
}
|
|
sort.Slice(usdStats.HoldingValues, func(i, j int) bool {
|
|
return usdStats.HoldingValues[i].Time.Before(usdStats.HoldingValues[j].Time)
|
|
})
|
|
|
|
if len(usdStats.HoldingValues) == 0 {
|
|
return nil, fmt.Errorf("%w and holding values", errMissingSnapshots)
|
|
}
|
|
|
|
usdStats.HoldingValueDifference = report.FinalFunds.Sub(report.InitialFunds).Div(report.InitialFunds).Mul(decimal.NewFromInt(100))
|
|
|
|
riskFreeRatePerCandle := usdStats.RiskFreeRate.Div(decimal.NewFromFloat(interval.IntervalsPerYear()))
|
|
returnsPerCandle := make([]decimal.Decimal, len(usdStats.HoldingValues))
|
|
benchmarkRates := make([]decimal.Decimal, len(usdStats.HoldingValues))
|
|
benchmarkMovement := usdStats.HoldingValues[0].Value
|
|
benchmarkRates[0] = usdStats.HoldingValues[0].Value
|
|
for j := range usdStats.HoldingValues {
|
|
if j != 0 && !usdStats.HoldingValues[j-1].Value.IsZero() {
|
|
benchmarkMovement = benchmarkMovement.Add(benchmarkMovement.Mul(riskFreeRatePerCandle))
|
|
benchmarkRates[j] = riskFreeRatePerCandle
|
|
returnsPerCandle[j] = usdStats.HoldingValues[j].Value.Sub(usdStats.HoldingValues[j-1].Value).Div(usdStats.HoldingValues[j-1].Value)
|
|
}
|
|
}
|
|
benchmarkRates = benchmarkRates[1:]
|
|
returnsPerCandle = returnsPerCandle[1:]
|
|
if !usdStats.HoldingValues[0].Value.IsZero() {
|
|
usdStats.BenchmarkMarketMovement = benchmarkMovement.Sub(usdStats.HoldingValues[0].Value).Div(usdStats.HoldingValues[0].Value).Mul(decimal.NewFromInt(100))
|
|
}
|
|
usdStats.MaxDrawdown, err = CalculateBiggestValueAtTimeDrawdown(usdStats.HoldingValues, interval)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sep := "USD Totals |\t"
|
|
usdStats.ArithmeticRatios, usdStats.GeometricRatios, err = CalculateRatios(benchmarkRates, returnsPerCandle, riskFreeRatePerCandle, &usdStats.MaxDrawdown, sep)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var cagr decimal.Decimal
|
|
for i := range response.Items {
|
|
if response.Items[i].ReportItem.InitialFunds.IsZero() {
|
|
continue
|
|
}
|
|
cagr, err = gctmath.DecimalCompoundAnnualGrowthRate(
|
|
response.Items[i].ReportItem.InitialFunds,
|
|
response.Items[i].ReportItem.FinalFunds,
|
|
decimal.NewFromFloat(interval.IntervalsPerYear()),
|
|
decimal.NewFromInt(int64(len(usdStats.HoldingValues))),
|
|
)
|
|
if err != nil && !errors.Is(err, gctmath.ErrPowerDifferenceTooSmall) {
|
|
return nil, err
|
|
}
|
|
response.Items[i].CompoundAnnualGrowthRate = cagr
|
|
}
|
|
if !usdStats.HoldingValues[0].Value.IsZero() {
|
|
cagr, err = gctmath.DecimalCompoundAnnualGrowthRate(
|
|
usdStats.HoldingValues[0].Value,
|
|
usdStats.HoldingValues[len(usdStats.HoldingValues)-1].Value,
|
|
decimal.NewFromFloat(interval.IntervalsPerYear()),
|
|
decimal.NewFromInt(int64(len(usdStats.HoldingValues))),
|
|
)
|
|
if err != nil && !errors.Is(err, gctmath.ErrPowerDifferenceTooSmall) {
|
|
return nil, err
|
|
}
|
|
usdStats.CompoundAnnualGrowthRate = cagr
|
|
}
|
|
usdStats.DidStrategyMakeProfit = report.FinalFunds.GreaterThan(report.InitialFunds)
|
|
usdStats.DidStrategyBeatTheMarket = usdStats.HoldingValueDifference.GreaterThan(usdStats.BenchmarkMarketMovement)
|
|
response.TotalUSDStatistics = usdStats
|
|
|
|
return response, nil
|
|
}
|
|
|
|
// CalculateIndividualFundingStatistics calculates statistics for an individual report item
|
|
func CalculateIndividualFundingStatistics(disableUSDTracking bool, reportItem *funding.ReportItem, relatedStats []relatedCurrencyPairStatistics) (*FundingItemStatistics, error) {
|
|
if reportItem == nil {
|
|
return nil, fmt.Errorf("%w - nil report item", gctcommon.ErrNilPointer)
|
|
}
|
|
|
|
item := &FundingItemStatistics{
|
|
ReportItem: reportItem,
|
|
}
|
|
if disableUSDTracking || reportItem.AppendedViaAPI {
|
|
return item, nil
|
|
}
|
|
closePrices := reportItem.Snapshots
|
|
if len(closePrices) == 0 {
|
|
return nil, errMissingSnapshots
|
|
}
|
|
item.StartingClosePrice = ValueAtTime{
|
|
Time: closePrices[0].Time,
|
|
Value: closePrices[0].USDClosePrice,
|
|
}
|
|
item.EndingClosePrice = ValueAtTime{
|
|
Time: closePrices[len(closePrices)-1].Time,
|
|
Value: closePrices[len(closePrices)-1].USDClosePrice,
|
|
}
|
|
for i := range closePrices {
|
|
if (closePrices[i].USDClosePrice.LessThan(item.LowestClosePrice.Value) || !item.LowestClosePrice.Set) && !closePrices[i].USDClosePrice.IsZero() {
|
|
item.LowestClosePrice.Value = closePrices[i].USDClosePrice
|
|
item.LowestClosePrice.Time = closePrices[i].Time
|
|
item.LowestClosePrice.Set = true
|
|
}
|
|
if closePrices[i].USDClosePrice.GreaterThan(item.HighestClosePrice.Value) || !item.HighestClosePrice.Set {
|
|
item.HighestClosePrice.Value = closePrices[i].USDClosePrice
|
|
item.HighestClosePrice.Time = closePrices[i].Time
|
|
item.HighestClosePrice.Set = true
|
|
}
|
|
}
|
|
item.IsCollateral = reportItem.IsCollateral
|
|
if reportItem.Asset.IsFutures() {
|
|
var lowest, highest, initial, final ValueAtTime
|
|
initial.Value = closePrices[0].Available
|
|
initial.Time = closePrices[0].Time
|
|
final.Value = closePrices[len(closePrices)-1].Available
|
|
final.Time = closePrices[len(closePrices)-1].Time
|
|
for i := range closePrices {
|
|
if closePrices[i].Available.LessThan(lowest.Value) || !lowest.Set {
|
|
lowest.Value = closePrices[i].Available
|
|
lowest.Time = closePrices[i].Time
|
|
lowest.Set = true
|
|
}
|
|
if closePrices[i].Available.GreaterThan(highest.Value) || !lowest.Set {
|
|
highest.Value = closePrices[i].Available
|
|
highest.Time = closePrices[i].Time
|
|
highest.Set = true
|
|
}
|
|
}
|
|
if reportItem.IsCollateral {
|
|
item.LowestCollateral = lowest
|
|
item.HighestCollateral = highest
|
|
item.InitialCollateral = initial
|
|
item.FinalCollateral = final
|
|
} else {
|
|
item.LowestHoldings = lowest
|
|
item.HighestHoldings = highest
|
|
item.InitialHoldings = initial
|
|
item.FinalHoldings = final
|
|
}
|
|
}
|
|
if !reportItem.IsCollateral {
|
|
for i := range relatedStats {
|
|
if relatedStats[i].stat == nil {
|
|
return nil, fmt.Errorf("%w related stats", gctcommon.ErrNilPointer)
|
|
}
|
|
if relatedStats[i].isBaseCurrency {
|
|
item.BuyOrders += relatedStats[i].stat.BuyOrders
|
|
item.SellOrders += relatedStats[i].stat.SellOrders
|
|
}
|
|
}
|
|
}
|
|
|
|
item.TotalOrders = item.BuyOrders + item.SellOrders
|
|
if !item.ReportItem.ShowInfinite && !reportItem.IsCollateral {
|
|
if item.ReportItem.Snapshots[0].USDValue.IsZero() {
|
|
item.ReportItem.ShowInfinite = true
|
|
} else {
|
|
item.StrategyMovement = item.ReportItem.USDFinalFunds.Sub(
|
|
item.ReportItem.USDInitialFunds).Div(
|
|
item.ReportItem.USDInitialFunds).Mul(
|
|
decimal.NewFromInt(100))
|
|
}
|
|
}
|
|
|
|
if !item.ReportItem.Snapshots[0].USDClosePrice.IsZero() {
|
|
item.MarketMovement = item.ReportItem.Snapshots[len(item.ReportItem.Snapshots)-1].USDClosePrice.Sub(
|
|
item.ReportItem.Snapshots[0].USDClosePrice).Div(
|
|
item.ReportItem.Snapshots[0].USDClosePrice).Mul(
|
|
decimal.NewFromInt(100))
|
|
}
|
|
if !reportItem.IsCollateral {
|
|
item.DidStrategyBeatTheMarket = item.StrategyMovement.GreaterThan(item.MarketMovement)
|
|
}
|
|
item.HighestCommittedFunds = ValueAtTime{}
|
|
for j := range item.ReportItem.Snapshots {
|
|
if item.ReportItem.Snapshots[j].USDValue.GreaterThan(item.HighestCommittedFunds.Value) {
|
|
item.HighestCommittedFunds = ValueAtTime{
|
|
Time: item.ReportItem.Snapshots[j].Time,
|
|
Value: item.ReportItem.Snapshots[j].USDValue,
|
|
}
|
|
}
|
|
}
|
|
if item.ReportItem.USDPairCandle == nil && !reportItem.IsCollateral {
|
|
return nil, fmt.Errorf("%w usd candles missing", errMissingSnapshots)
|
|
}
|
|
s, err := item.ReportItem.USDPairCandle.GetStream()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(s) == 0 {
|
|
return nil, fmt.Errorf("%w stream missing", errMissingSnapshots)
|
|
}
|
|
if reportItem.IsCollateral {
|
|
return item, nil
|
|
}
|
|
item.MaxDrawdown, err = CalculateBiggestEventDrawdown(s)
|
|
return item, err
|
|
}
|