Skip to content
7 changes: 4 additions & 3 deletions analysis.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,10 @@ type LogTradesAnalysis struct {
// Analyze logs trades to provided io.Writer
func (lta LogTradesAnalysis) Analyze(record *TradingRecord) float64 {
logOrder := func(trade *Position) {
fmt.Fprintln(lta.Writer, fmt.Sprintf("%s - enter with buy %s (%s @ $%s)", trade.EntranceOrder().ExecutionTime.UTC().Format(time.RFC822), trade.EntranceOrder().Security, trade.EntranceOrder().Amount, trade.EntranceOrder().Price))
fmt.Fprintln(lta.Writer, fmt.Sprintf("%s - exit with sell %s (%s @ $%s)", trade.ExitOrder().ExecutionTime.UTC().Format(time.RFC822), trade.ExitOrder().Security, trade.ExitOrder().Amount, trade.ExitOrder().Price))

fmt.Fprintln(lta.Writer, fmt.Sprintf("%s - enter with %s %s (%s @ $%s)",
trade.EntranceOrder().ExecutionTime.Format(time.RFC822), trade.EntranceOrder().Side, trade.EntranceOrder().Security, trade.EntranceOrder().Amount, trade.EntranceOrder().Price))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indentation is off here.

fmt.Fprintln(lta.Writer, fmt.Sprintf("%s - exit with %s %s (%s @ $%s)",
trade.ExitOrder().ExecutionTime.Format(time.RFC822), trade.EntranceOrder().Side, trade.ExitOrder().Security, trade.ExitOrder().Amount, trade.ExitOrder().Price))
profit := trade.ExitValue().Sub(trade.CostBasis())
fmt.Fprintln(lta.Writer, fmt.Sprintf("Profit: $%s", profit))
}
Expand Down
11 changes: 11 additions & 0 deletions order.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@ const (
SELL
)

func (os OrderSide) String() string {
switch os {
case BUY:
return "BUY"
case SELL:
return "SELL"
default:
return "UNKNOWN"
}
}

// Order represents a trade execution (buy or sell) with associated metadata.
type Order struct {
Side OrderSide
Expand Down
36 changes: 33 additions & 3 deletions rule_stop.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
package techan

import "github.com/sdcoffey/big"
import (
"github.com/sdcoffey/big"
)

type stopLossRule struct {
Indicator
tolerance big.Decimal
}

type stopGainRule struct {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You actually don't need to define a duplicate struct here, since this one is private. I would redefine the above rule as

type stopRule struct {
    Indicator
    tolerace big.Decimal
}

and then make an alias for both stopLoss rule and stopGain rule, eg.

type stopLossRule = stopRule
type stopGainRule = stopRule

Indicator
tolerance big.Decimal
}

// NewStopLossRule returns a new rule that is satisfied when the given loss tolerance (a percentage) is met or exceeded.
// Loss tolerance should be a value between -1 and 1.
func NewStopLossRule(series *TimeSeries, lossTolerance float64) Rule {
Expand All @@ -20,8 +27,31 @@ func (slr stopLossRule) IsSatisfied(index int, record *TradingRecord) bool {
if !record.CurrentPosition().IsOpen() {
return false
}

openPrice := record.CurrentPosition().CostBasis()
openPrice := record.CurrentPosition().EntranceOrder().Price
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like a breaking change. EntranceOrder().Price reflects the unit price you paid for a security, not the total value of the position. Even though I think what you're doing here isn't incorrect, it would be breaking change for anyone who was depending on this behavior.

loss := slr.Indicator.Calculate(index).Div(openPrice).Sub(big.ONE)
if record.CurrentPosition().IsShort() {
loss = loss.Neg()
}
return loss.LTE(slr.tolerance)
}

// NewStopLossRule returns a new rule that is satisfied when the given loss tolerance (a percentage) is met or exceeded.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docs are wrong here and on the line below

// Loss tolerance should be a value between -1 and 1.
func NewStopGainRule(series *TimeSeries, gainTolerance float64) Rule {
return stopGainRule{
Indicator: NewClosePriceIndicator(series),
tolerance: big.NewDecimal(gainTolerance),
}
}

func (sgr stopGainRule) IsSatisfied(index int, record *TradingRecord) bool {
if !record.CurrentPosition().IsOpen() {
return false
}
openPrice := record.CurrentPosition().EntranceOrder().Price
gain := sgr.Indicator.Calculate(index).Div(openPrice).Sub(big.ONE)
if record.CurrentPosition().IsShort() {
gain = gain.Neg()
}
return gain.GTE(sgr.tolerance)
}