Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 33 additions & 5 deletions analysis.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,12 @@ type PercentGainAnalysis struct{}
// Analyze analyzes the trading record for the percentage profit gained relative to start
func (pga PercentGainAnalysis) Analyze(record *TradingRecord) float64 {
if len(record.Trades) > 0 && record.Trades[0].IsClosed() {
return (record.Trades[len(record.Trades)-1].ExitValue().Div(record.Trades[0].CostBasis())).Sub(big.NewDecimal(1)).Float()
costBasis := record.Trades[0].CostBasis()
if costBasis.EQ(big.ZERO) {
return 0
}

return (record.Trades[len(record.Trades)-1].ExitValue().Div(costBasis)).Sub(big.NewDecimal(1)).Float()
}

return 0
Expand Down Expand Up @@ -92,10 +97,18 @@ type PeriodProfitAnalysis struct {

// Analyze returns the average profit for the trading record based on the given duration
func (ppa PeriodProfitAnalysis) Analyze(record *TradingRecord) float64 {
if len(record.Trades) == 0 || ppa.Period <= 0 {
return 0
}

var tp TotalProfitAnalysis
totalProfit := tp.Analyze(record)

periods := record.Trades[len(record.Trades)-1].ExitOrder().ExecutionTime.Sub(record.Trades[0].EntranceOrder().ExecutionTime) / ppa.Period
if periods == 0 {
return 0
}

return totalProfit / float64(periods)
}

Expand All @@ -106,10 +119,16 @@ type ProfitableTradesAnalysis struct{}
func (pta ProfitableTradesAnalysis) Analyze(record *TradingRecord) float64 {
var profitableTrades int
for _, trade := range record.Trades {
if !trade.IsClosed() {
continue
}

costBasis := trade.EntranceOrder().Amount.Mul(trade.EntranceOrder().Price)
sellPrice := trade.ExitOrder().Amount.Mul(trade.ExitOrder().Price)

if sellPrice.GT(costBasis) {
if trade.IsLong() && sellPrice.GT(costBasis) {
profitableTrades++
} else if trade.IsShort() && sellPrice.LT(costBasis) {
profitableTrades++
}
}
Expand All @@ -123,6 +142,10 @@ type AverageProfitAnalysis struct{}

// Analyze returns the average profit of the trading record
func (apa AverageProfitAnalysis) Analyze(record *TradingRecord) float64 {
if len(record.Trades) == 0 {
return 0
}

var tp TotalProfitAnalysis
totalProft := tp.Analyze(record)

Expand All @@ -139,14 +162,19 @@ type BuyAndHoldAnalysis struct {

// Analyze returns the profit based on a simple buy and hold strategy
func (baha BuyAndHoldAnalysis) Analyze(record *TradingRecord) float64 {
if len(record.Trades) == 0 {
if len(record.Trades) == 0 || baha.TimeSeries == nil || len(baha.TimeSeries.Candles) == 0 {
return 0
}

firstClose := baha.TimeSeries.Candles[0].ClosePrice
if firstClose.EQ(big.ZERO) {
return 0
}

openOrder := Order{
Side: BUY,
Amount: big.NewDecimal(baha.StartingMoney).Div(baha.TimeSeries.Candles[0].ClosePrice),
Price: baha.TimeSeries.Candles[0].ClosePrice,
Amount: big.NewDecimal(baha.StartingMoney).Div(firstClose),
Price: firstClose,
}

closeOrder := Order{
Expand Down
147 changes: 147 additions & 0 deletions analysis_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,28 @@ func TestPercentGainAnalysis(t *testing.T) {
gain := pga.Analyze(record)
assert.EqualValues(t, -.375, gain)
})

t.Run("Zero cost basis", func(t *testing.T) {
record := NewTradingRecord()

record.Operate(Order{
Side: BUY,
Amount: big.NewDecimal(1),
Price: big.ZERO,
Security: example,
ExecutionTime: time.Now(),
})
record.Operate(Order{
Side: SELL,
Amount: big.NewDecimal(1),
Price: big.NewDecimal(1),
Security: example,
ExecutionTime: time.Now(),
})

pga := PercentGainAnalysis{}
assert.EqualValues(t, 0, pga.Analyze(record))
})
}

func TestNumTradesAnalysis(t *testing.T) {
Expand Down Expand Up @@ -283,6 +305,59 @@ func TestLogTradesAnalysis(t *testing.T) {
}

func TestPeriodProfitAnalysis(t *testing.T) {
t.Run("Zero when there are no trades", func(t *testing.T) {
ppa := PeriodProfitAnalysis{
Period: time.Minute,
}

assert.EqualValues(t, 0, ppa.Analyze(NewTradingRecord()))
})

t.Run("Zero when period is invalid", func(t *testing.T) {
record := NewTradingRecord()
record.Operate(Order{
Side: BUY,
Amount: big.NewDecimal(1),
Price: big.NewDecimal(1),
Security: example,
ExecutionTime: time.Now(),
})
record.Operate(Order{
Side: SELL,
Amount: big.NewDecimal(2),
Price: big.NewDecimal(1),
Security: example,
ExecutionTime: time.Now(),
})

assert.EqualValues(t, 0, PeriodProfitAnalysis{}.Analyze(record))
})

t.Run("Zero when no full periods have elapsed", func(t *testing.T) {
record := NewTradingRecord()
now := time.Now()
record.Operate(Order{
Side: BUY,
Amount: big.NewDecimal(1),
Price: big.NewDecimal(1),
Security: example,
ExecutionTime: now,
})
record.Operate(Order{
Side: SELL,
Amount: big.NewDecimal(2),
Price: big.NewDecimal(1),
Security: example,
ExecutionTime: now.Add(time.Minute),
})

ppa := PeriodProfitAnalysis{
Period: time.Hour,
}

assert.EqualValues(t, 0, ppa.Analyze(record))
})

record := NewTradingRecord()

now := time.Now().Add(-time.Minute * 5)
Expand Down Expand Up @@ -370,9 +445,56 @@ func TestProfitableTradesAnalysis(t *testing.T) {
pta := ProfitableTradesAnalysis{}

assert.EqualValues(t, 1, pta.Analyze(record))

t.Run("Counts profitable short trades", func(t *testing.T) {
record := NewTradingRecord()

orders := []Order{
{
Side: SELL,
Amount: big.NewDecimal(1),
Price: big.NewDecimal(10),
Security: example,
ExecutionTime: time.Now(),
},
{
Side: BUY,
Amount: big.NewDecimal(1),
Price: big.NewDecimal(8),
Security: example,
ExecutionTime: time.Now(),
},
{
Side: SELL,
Amount: big.NewDecimal(1),
Price: big.NewDecimal(10),
Security: example,
ExecutionTime: time.Now(),
},
{
Side: BUY,
Amount: big.NewDecimal(1),
Price: big.NewDecimal(12),
Security: example,
ExecutionTime: time.Now(),
},
}

for _, order := range orders {
record.Operate(order)
}

assert.EqualValues(t, 1, pta.Analyze(record))
})
}

func TestAverageProfitAnalysis(t *testing.T) {
t.Run("Zero when there are no trades", func(t *testing.T) {
pta := AverageProfitAnalysis{}

assert.EqualValues(t, 0, pta.Analyze(NewTradingRecord()))
})

record := NewTradingRecord()

orders := []Order{
Expand Down Expand Up @@ -471,4 +593,29 @@ func TestBuyAndHoldAnalysis(t *testing.T) {

assert.EqualValues(t, 5, buyAndHoldAnalysis.Analyze(record))
})

t.Run("== 0 candles returns zero", func(t *testing.T) {
record := NewTradingRecord()
record.Operate(Order{
Side: BUY,
Amount: big.NewDecimal(1),
Price: big.NewDecimal(1),
Security: example,
ExecutionTime: time.Now(),
})
record.Operate(Order{
Side: SELL,
Amount: big.NewDecimal(2),
Price: big.NewDecimal(1),
Security: example,
ExecutionTime: time.Now(),
})

buyAndHoldAnalysis := BuyAndHoldAnalysis{
TimeSeries: NewTimeSeries(),
StartingMoney: 1,
}

assert.EqualValues(t, 0, buyAndHoldAnalysis.Analyze(record))
})
}
18 changes: 4 additions & 14 deletions testutils.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,24 +88,14 @@ func decimalEquals(t *testing.T, expected float64, actual big.Decimal) {
assert.Equal(t, fmt.Sprintf("%.4f", expected), fmt.Sprintf("%.4f", actual.Float()))
}

func dump(indicator Indicator) (values []float64) {
func indicatorEquals(t *testing.T, expected []float64, indicator Indicator) {
precision := 4.0
m := math.Pow(10, precision)

defer func() {
recover()
}()

var index int
for {
values = append(values, math.Round(indicator.Calculate(index).Float()*m)/m)
index++
actualValues := make([]float64, len(expected))
for i := range expected {
actualValues[i] = math.Round(indicator.Calculate(i).Float()*m) / m
}

return
}

func indicatorEquals(t *testing.T, expected []float64, indicator Indicator) {
actualValues := dump(indicator)
assert.EqualValues(t, expected, actualValues)
}
Loading