diff --git a/config.yml b/config.yml index cfb9a17..9e4d884 100644 --- a/config.yml +++ b/config.yml @@ -2,8 +2,8 @@ #バックテストの基本設定 -assetName: "SOLUSDT" -duration: "15m" +assetName: "ETHUSDT" +duration: "30m" start: "" end: "" simpleInterest: false diff --git a/pkg/analytics/profit_and_loss.go b/pkg/analytics/profit_and_loss.go index 78c8166..f0f37f9 100644 --- a/pkg/analytics/profit_and_loss.go +++ b/pkg/analytics/profit_and_loss.go @@ -375,6 +375,7 @@ func PLSlice(s *execute.SignalEvents) []float64 { var pl []float64 // profit or loss slice var buyPrice float64 var sellPrice float64 + var lastSide string if s.Signals == nil || len(s.Signals) == 0 { return nil @@ -386,26 +387,75 @@ func PLSlice(s *execute.SignalEvents) []float64 { } if signal.Side == "BUY" { buyPrice = signal.Price - // if there is a previous sell price, calculate the profit or loss - if sellPrice != 0 { - pl = append(pl, (sellPrice-buyPrice)*signal.Size) - // reset the sell price - buyPrice = 0 - } + lastSide = "BUY" + } if signal.Side == "SELL" { sellPrice = signal.Price - // if there is a previous buy price, calculate the profit or loss - if buyPrice != 0 { + lastSide = "SELL" + + } + if signal.Side == "CLOSE" { + if lastSide == "BUY" { + pl = append(pl, (sellPrice-buyPrice)*signal.Size) + buyPrice = 0 + } + if lastSide == "SELL" { pl = append(pl, (sellPrice-buyPrice)*signal.Size) // reset the buy price sellPrice = 0 } + lastSide = "" } } return pl } +// func TotalProfitSlice(s *execute.SignalEvents) []float64 { +// if s == nil { +// return nil +// } +// var profit []float64 // total profit slice +// var buyPrice float64 +// var sellPrice float64 +// var longProfit float64 +// var shortProfit float64 + +// if s.Signals == nil || len(s.Signals) == 0 { +// return nil +// } +// for _, signal := range s.Signals { + +// if signal.Side != "BUY" && signal.Side != "SELL" && signal.Side != "CLOSE" { +// return nil +// } +// if signal.Side == "BUY" { +// buyPrice = signal.Price +// // if there is a previous sell price, calculate the short profit +// if sellPrice != 0 { +// shortProfit = (sellPrice - buyPrice) * signal.Size +// profit = append(profit, shortProfit) +// // reset the sell price and short profit +// sellPrice = 0 +// shortProfit = 0 +// } +// } +// if signal.Side == "SELL" { +// sellPrice = signal.Price +// // if there is a previous buy price, calculate the long profit +// if buyPrice != 0 { +// longProfit = (sellPrice - buyPrice) * signal.Size +// profit = append(profit, longProfit) +// // reset the buy price and long profit +// buyPrice = 0 +// longProfit = 0 +// } +// } +// } + +// return profit +// } + func TotalProfitSlice(s *execute.SignalEvents) []float64 { if s == nil { return nil @@ -415,6 +465,7 @@ func TotalProfitSlice(s *execute.SignalEvents) []float64 { var sellPrice float64 var longProfit float64 var shortProfit float64 + var lastSide string if s.Signals == nil || len(s.Signals) == 0 { return nil @@ -426,26 +477,35 @@ func TotalProfitSlice(s *execute.SignalEvents) []float64 { } if signal.Side == "BUY" { buyPrice = signal.Price - // if there is a previous sell price, calculate the short profit - if sellPrice != 0 { - shortProfit = (sellPrice - buyPrice) * signal.Size - profit = append(profit, shortProfit) - // reset the sell price and short profit - sellPrice = 0 - shortProfit = 0 - } + lastSide = "BUY" } if signal.Side == "SELL" { sellPrice = signal.Price - // if there is a previous buy price, calculate the long profit - if buyPrice != 0 { - longProfit = (sellPrice - buyPrice) * signal.Size + lastSide = "SELL" + } + + if signal.Side == "CLOSE" { + // if the last opened order side is BUY, calculate the long profit + if lastSide == "BUY" { + longProfit = (signal.Price - buyPrice) * signal.Size profit = append(profit, longProfit) // reset the buy price and long profit buyPrice = 0 longProfit = 0 } + // if the last opened order side is SELL, calculate the short profit + if lastSide == "SELL" { + shortProfit = (sellPrice - signal.Price) * signal.Size + profit = append(profit, shortProfit) + // reset the sell price and short profit + sellPrice = 0 + shortProfit = 0 + } + // reset the last opened order side + lastSide = "" + } + } return profit diff --git a/pkg/strategey/buy&hold.go b/pkg/strategey/buy&hold.go index 5b9460a..3189d6f 100644 --- a/pkg/strategey/buy&hold.go +++ b/pkg/strategey/buy&hold.go @@ -35,7 +35,7 @@ func BuyAndHoldingStrategy(account *trader.Account) (profit float64, multiple fl buySize := account.TradeSize(1) account.HolderBuy(close[0], buySize) - account.Exit(close[lenCandles-1]) + account.HolderSell(close[lenCandles-1]) profit = account.Balance - initialBalance multiple = account.Balance / initialBalance diff --git a/pkg/strategey/donchain_choppy.go b/pkg/strategey/donchain_choppy.go new file mode 100644 index 0000000..3b240fd --- /dev/null +++ b/pkg/strategey/donchain_choppy.go @@ -0,0 +1,299 @@ +package strategey + +// import ( +// "fmt" +// "runtime" +// "sync" +// "v1/pkg/analytics" +// "v1/pkg/execute" +// "v1/pkg/indicator/indicators" +// "v1/pkg/management/risk" +// "v1/pkg/trader" + +// "github.com/google/uuid" +// "github.com/markcheno/go-talib" +// ) + +// // func getStrageyNameDonchain() string { +// // return "DBO" +// // } + +// type DCStrategy struct { +// StrategyName string +// Donchain indicators.Donchan +// SignalEvents *execute.SignalEvents +// Ema []float64 +// ChoppyEma []float64 +// ChoppyIndex []float64 +// BuySignalId uuid.UUID +// SellSignalId uuid.UUID +// BuySize float64 +// SellSize float64 +// BuyPrice float64 +// SellPrice float64 +// LongSlRatio float64 +// ShortSlRatio float64 +// IsBuyHolding bool +// IsSellHolding bool +// } + +// func NewDCStrategy() *DCStrategy { + +// return &DCStrategy{} +// } + +// func (df *DataFrameCandle) DCStrategySettings(period int, choppy int, duration int) *DCStrategy { +// dc := NewDCStrategy() + +// // 指定された値を代入する +// dc.StrategyName = "DBO_CHOPPY" +// dc.SignalEvents = execute.NewSignalEvents() +// dc.Donchain = indicators.Donchain(df.Highs(), df.Lows(), period) +// dc.Ema = talib.Ema(df.Hlc3(), 89) +// dc.BuySignalId = uuid.New() // uuidはランダムに生成する +// dc.SellSignalId = uuid.New() +// dc.BuySize = 0.0 +// dc.SellSize = 0.0 +// dc.BuyPrice = 0.0 +// dc.SellPrice = 0.0 +// dc.LongSlRatio = 0.9 +// dc.ShortSlRatio = 1.1 +// dc.IsBuyHolding = false +// dc.IsSellHolding = false +// dc.ChoppyIndex = risk.ChoppySlice(duration, df.Closes(), df.Highs(), df.Lows()) +// dc.ChoppyEma = risk.ChoppyEma(dc.ChoppyIndex, choppy) + +// // 作成したインスタンスを返す +// return dc + +// } + +// func (df *DataFrameCandle) DonchainChoppyStrategy(period int, choppy int, duration int, account *trader.Account, simple bool) *execute.SignalEvents { +// var StrategyName = "DBO_CHOPPY" + +// lenCandles := len(df.Candles) +// if lenCandles <= period { +// return nil +// } + +// signalEvents := execute.NewSignalEvents() +// dc := df.DCStrategySettings(period, choppy, duration) +// c := df.Closes() + +// for i := 30; i < lenCandles; i++ { + +// if i < period || i >= len(dc.ChoppyEma) { +// continue +// } + +// if c[i] > dc.Donchain.High[i-1] && dc.ChoppyEma[i] > 50 && c[i] > dc.Ema[i] && !dc.IsBuyHolding { +// // fee := 1 - 0.01 +// if simple { +// if dc.IsSellHolding { + +// accountBalance := account.GetBalance() +// signalEvents.Close(dc.SellSignalId, StrategyName, df.AssetName, df.Duration, df.Candles[i].Date, df.Candles[i].Close, sellSize, accountBalance, false) +// dc.IsSellHolding = false +// } else { +// buySize = account.SimpleTradeSize(1) +// buyPrice = close[i] +// accountBalance := account.GetBalance() + +// signalEvents.Buy(StrategyName, df.AssetName, df.Duration, df.Candles[i].Date, df.Candles[i].Close, buySize, accountBalance, false) +// isBuyHolding = true + +// } + +// } else { +// buySize = account.TradeSize(riskSize) / df.Candles[i].Close +// buyPrice = close[i] +// accountBalance := account.GetBalance() +// if account.Entry(df.Candles[i].Close, buySize) { +// signalEvents.Buy(StrategyName, df.AssetName, df.Duration, df.Candles[i].Date, df.Candles[i].Close, buySize, accountBalance, false) +// isBuyHolding = true +// } +// } + +// } +// if (close[i] < donchain.Low[i-1] || (close[i] <= buyPrice*slRatio)) && isHolding { + +// if simple { +// accountBalance := 1000.0 + +// signalEvents.Sell(StrategyName, df.AssetName, df.Duration, df.Candles[i].Date, df.Candles[i].Close, buySize, accountBalance, false) +// isSellHolding = true + +// } else { +// accountBalance := account.GetBalance() +// if account.Exit(df.Candles[i].Close) { +// signalEvents.Sell(StrategyName, df.AssetName, df.Duration, df.Candles[i].Date, df.Candles[i].Close, buySize, accountBalance, false) +// isSellHolding = true +// buySize = 0.0 +// account.PositionSize = buySize + +// } +// } + +// } + +// } +// return signalEvents + +// } + +// func (df *DataFrameCandle) OptimizeDonchain() (performance float64, bestPeriod int, bestChoppy int, bestDuration int) { +// runtime.GOMAXPROCS(runtime.NumCPU()) +// bestPeriod = 40 +// bestChoppy = 13 +// var mu sync.Mutex +// var wg sync.WaitGroup + +// // a := trader.NewAccount(1000) +// // marketDefault, _ := BuyAndHoldingStrategy(a) + +// limit := 3000 +// slots := make(chan struct{}, limit) + +// for period := 20; period < 300; period += 5 { +// for duration := 30; duration < 80; duration += 10 { +// for choppy := 3; choppy < 11; choppy += 2 { +// wg.Add(1) +// slots <- struct{}{} + +// go func(period int, choppy int, duration int) { +// defer wg.Done() +// account := trader.NewAccount(1000) +// signalEvents := df.DonchainChoppyStrategy(period, choppy, duration, account, simple) + +// if signalEvents == nil { +// return +// } + +// // if analytics.TotalTrades(signalEvents) < 10 { +// // <-slots +// // return +// // } + +// // if analytics.NetProfit(signalEvents) < marketDefault { +// // // <-slots +// // return +// // } + +// // if analytics.SQN(signalEvents) < 3.2 { +// // <-slots +// // return +// // } + +// // if analytics.ProfitFactor(signalEvents) < 3 { +// // <-slots +// // return +// // } + +// pf := analytics.SortinoRatio(signalEvents, 0.02) +// // pf := analytics.Prr(signalEvents) +// mu.Lock() +// if performance < pf { +// performance = pf +// bestPeriod = period +// bestChoppy = choppy +// bestDuration = duration +// } +// <-slots +// mu.Unlock() +// }(period, choppy, duration) +// } +// } +// } + +// wg.Wait() + +// fmt.Println("最高パフォーマンス", performance, "最適なピリオド", bestPeriod, "最適なチョッピー", bestChoppy, "最適なチョッピー期間", bestDuration) + +// return performance, bestPeriod, bestChoppy, bestDuration +// } + +// // func (df *DataFrameCandle) OptimizeDonchain() (performance float64, bestPeriod int, bestChoppy int, bestDuration int) { + +// // // オブジェクティブ関数を定義 +// // objective := func(trial goptuna.Trial) (float64, error) { +// // // ハイパーパラメータの候補をサンプリング +// // period, _ := trial.SuggestStepInt("atrPeriod", 20, 300, 10) +// // choppy, _ := trial.SuggestStepInt("choppy", 5, 18, 1) +// // duration, _ := trial.SuggestStepInt("duration", 10, 200, 10) + +// // account := trader.NewAccount(1000) // Move this line inside the objective function +// // signalEvents := df.DonchainChoppyStrategy(period, choppy, duration, account, simple) + +// // if signalEvents == nil { +// // return 0.0, nil +// // } + +// // if analytics.TotalTrades(signalEvents) < 10 { +// // return 0.0, nil +// // } + +// // // p := analytics.SortinoRatio(signalEvents, 0.02) +// // p := analytics.Prr(signalEvents) +// // return p, nil // パフォーマンスを返す +// // } + +// // // ベイズ最適化の設定 +// // study, err := goptuna.CreateStudy( +// // "donchain-choppy-optimization", +// // goptuna.StudyOptionSampler(tpe.NewSampler()), // 獲得関数としてTPEを使用 +// // goptuna.StudyOptionDirection(goptuna.StudyDirectionMaximize), // 最大化問題として定義 +// // goptuna.StudyOptionLogger(nil), +// // ) +// // if err != nil { +// // panic(err) +// // } + +// // // ベイズ最適化の実行 +// // err = study.Optimize(objective, 800) +// // if err != nil { +// // panic(err) +// // } + +// // // 最適化結果の取得 +// // v, _ := study.GetBestValue() +// // params, _ := study.GetBestParams() +// // performance = v +// // bestPeriod = params["atrPeriod"].(int) +// // bestChoppy = params["choppy"].(int) +// // bestDuration = params["duration"].(int) + +// // fmt.Println("最高パフォーマンス", performance, "最適なピリオド", bestPeriod, "最適なチョッピー", bestChoppy, "最適なチョッピー期間", bestDuration) + +// // return performance, bestPeriod, bestChoppy, bestDuration +// // } + +// func RunDonchainOptimize() { + +// df, account, _ := RadyBacktest() + +// p, bestPeriod, bestChoppy, bestDuration := df.OptimizeDonchain() + +// if p > 0 { + +// df.Signal = df.DonchainChoppyStrategy(bestPeriod, bestChoppy, bestDuration, account, simple) +// Result(df.Signal) + +// } else { +// fmt.Println("💸マイナスです") +// df.Signal = df.DonchainChoppyStrategy(bestPeriod, bestChoppy, bestDuration, account, simple) +// Result(df.Signal) + +// } + +// } + +// func DonchainBacktest() { + +// df, account, _ := RadyBacktest() + +// df.Signal = df.DonchainChoppyStrategy(15, 16, 30, account, simple) +// if df.Signal.Signals == nil { +// fmt.Println("トレード結果がありません") +// } +// Result(df.Signal) +// } diff --git a/pkg/strategey/ema_rsi.go b/pkg/strategey/ema_rsi.go index d3b26b6..ca9a961 100644 --- a/pkg/strategey/ema_rsi.go +++ b/pkg/strategey/ema_rsi.go @@ -53,80 +53,58 @@ func (df *DataFrameCandle) EmaRsiStrategy(period1 int, account *trader.Account, if ema1[i] < c[i] && rsi[i-1] < 30 && rsi[i] >= 30 && choppyEma[i] > 50 && !isBuyHolding { - // fee := 1 - 0.01 - if simple { - buySize = account.SimpleTradeSize(1) - buyPrice = c[i] - accountBalance := account.GetBalance() - buySignalId = uuid.New() - + buyPrice = c[i] + buySize = account.TradeSize(riskSize) / c[i] + buySignalId = uuid.New() + accountBalance := account.GetBalance() + if account.Entry("BUY", buyPrice, buySize, 0.01) { signalEvents.Buy(buySignalId, StrategyName, df.AssetName, df.Duration, df.Candles[i].Date, df.Candles[i].Close, buySize, accountBalance, false) isSellHolding = false isBuyHolding = true - } else { - - buyPrice = c[i] - buySize = account.TradeSize(riskSize) / c[i] - buySignalId = uuid.New() - accountBalance := account.GetBalance() - if account.Entry(buyPrice, buySize) { - signalEvents.Buy(buySignalId, StrategyName, df.AssetName, df.Duration, df.Candles[i].Date, df.Candles[i].Close, buySize, accountBalance, false) - isSellHolding = false - isBuyHolding = true - - } } + } - if (ema1[i-1] < c[i-1] && ema1[i] >= c[i] || c[i] <= sellPrice*shortSlRatio) && isBuyHolding { - if simple { + if (ema1[i] > c[i] || c[i] <= sellPrice*shortSlRatio) && isBuyHolding { + + if account.Exit("BUY", c[i]) { accountBalance := account.GetBalance() signalEvents.Close(buySignalId, StrategyName, df.AssetName, df.Duration, df.Candles[i].Date, df.Candles[i].Close, buySize, accountBalance, false) isBuyHolding = false - } else { - if account.Exit(c[i]) { - // accountBalance := account.GetBalance() - signalEvents.Close(buySignalId, StrategyName, df.AssetName, df.Duration, df.Candles[i].Date, df.Candles[i].Close, buySize, account.GetBalance(), false) - isBuyHolding = false - buySize = 0.0 - } + isSellHolding = false + buySize = 0.0 + account.PositionSize = buySize } - } + } + //SELLのエントリーかエグジットに問題がある。 if ema1[i] > c[i] && rsi[i-1] > 75 && rsi[i] <= 75 && choppyEma[i] > 50 && !isSellHolding { - if simple { - sellSize = account.SimpleTradeSize(1) - sellPrice = c[i] - sellSignalId = uuid.New() - signalEvents.Sell(sellSignalId, StrategyName, df.AssetName, df.Duration, df.Candles[i].Date, df.Candles[i].Close, sellSize, account.GetBalance(), false) + + sellPrice = c[i] + sellSize = account.TradeSize(riskSize) / c[i] + accountBalance := account.GetBalance() + sellSignalId = uuid.New() + if account.Entry("SELL", sellPrice, sellSize, 0.01) { + signalEvents.Sell(sellSignalId, StrategyName, df.AssetName, df.Duration, df.Candles[i].Date, df.Candles[i].Close, sellSize, accountBalance, false) isBuyHolding = false isSellHolding = true - } else { - sellPrice = c[i] - sellSize = account.TradeSize(riskSize) / c[i] - - sellSignalId = uuid.New() - if account.Entry(sellPrice, sellSize) { - signalEvents.Sell(sellSignalId, StrategyName, df.AssetName, df.Duration, df.Candles[i].Date, df.Candles[i].Close, sellSize, account.GetBalance(), false) - isBuyHolding = false - isSellHolding = true - account.PositionSize = buySize + // account.PositionSize = buySize - } } - if (ema1[i-1] > c[i-1] && ema1[i] <= c[i] || (c[i] <= buyPrice*longSlRatio)) && isSellHolding { - if simple { - signalEvents.Close(sellSignalId, StrategyName, df.AssetName, df.Duration, df.Candles[i].Date, df.Candles[i].Close, sellSize, account.GetBalance(), false) + if (ema1[i] < c[i] || (c[i] <= buyPrice*longSlRatio)) && isSellHolding { + + if account.Exit("SELL", c[i]) { + sellSize = account.PositionSize + accountBalance := account.GetBalance() + signalEvents.Close(sellSignalId, StrategyName, df.AssetName, df.Duration, df.Candles[i].Date, df.Candles[i].Close, sellSize, accountBalance, false) isSellHolding = false - } else { - if account.Exit(c[i]) { - signalEvents.Close(sellSignalId, StrategyName, df.AssetName, df.Duration, df.Candles[i].Date, df.Candles[i].Close, sellSize, account.GetBalance(), false) - isSellHolding = false - sellSize = 0 - } + isBuyHolding = false + sellSize = 0 + account.PositionSize = sellSize } + } } } @@ -174,7 +152,7 @@ func (df *DataFrameCandle) OptimizeEmaRsi2() (performance float64, bestPeriod in } // ベイズ最適化の実行 - err = study.Optimize(objective, 1000) + err = study.Optimize(objective, 500) if err != nil { panic(err) } @@ -192,13 +170,13 @@ func (df *DataFrameCandle) OptimizeEmaRsi2() (performance float64, bestPeriod in func (df *DataFrameCandle) OptimizeEmaRsi() (performance float64, bestPeriod int) { runtime.GOMAXPROCS(runtime.NumCPU()) - bestPeriod = 100 + bestPeriod = 10 limit := 1000 slots := make(chan struct{}, limit) - a := trader.NewAccount(1000) - marketDefault, _ := BuyAndHoldingStrategy(a) + // a := trader.NewAccount(1000) + // marketDefault, _ := BuyAndHoldingStrategy(a) var mu sync.Mutex var wg sync.WaitGroup @@ -222,10 +200,10 @@ func (df *DataFrameCandle) OptimizeEmaRsi() (performance float64, bestPeriod int return } - if analytics.TotalNetProfit(signalEvents) < marketDefault { - <-slots - return - } + // if analytics.TotalNetProfit(signalEvents) < marketDefault { + // <-slots + // return + // } // if analytics.SQN(signalEvents) < 3.2 { // <-slots @@ -243,6 +221,7 @@ func (df *DataFrameCandle) OptimizeEmaRsi() (performance float64, bestPeriod int mu.Lock() if performance == 0 || performance < p { performance = p + fmt.Println(performance) bestPeriod = period } diff --git a/pkg/trader/acount.go b/pkg/trader/acount.go index c50f121..896737b 100755 --- a/pkg/trader/acount.go +++ b/pkg/trader/acount.go @@ -5,6 +5,8 @@ package trader type Account struct { Balance float64 PositionSize float64 + BuyPrice float64 + SellPrice float64 } func NewAccount(initialBalance float64) *Account { @@ -24,23 +26,68 @@ func (a *Account) SimpleTradeSize(amount int) float64 { return size } -func (a *Account) Entry(price, size float64) bool { +// func (a *Account) Entry(price, size float64) bool { +// cost := price * size +// if cost > a.Balance { +// return false +// } +// if side == "BUY"{} +// a.Balance -= cost +// a.PositionSize = size +// a.BuyPrice = price // Update buy price +// a.SellPrice = price // Update sell price +// return true +// } + +// func (a *Account) Exit2(price float64) bool { +// if a.PositionSize <= 0 { +// return false +// } +// a.Balance += price * a.PositionSize +// a.PositionSize = 0.0 +// a.BuyPrice = 0.0 // Reset buy price +// a.SellPrice = 0.0 // Reset sell price +// return true +// } + +func (a *Account) Entry(positionType string, price, size, feeRate float64) bool { cost := price * size - if cost > a.Balance { + fee := cost * feeRate + + if cost+fee > a.Balance { return false } - a.Balance -= cost + + // a.Balance -= cost + fee a.PositionSize = size + if positionType == "BUY" { + a.BuyPrice = price + a.SellPrice = 0.0 + } else if positionType == "SELL" { + a.SellPrice = price + a.BuyPrice = 0.0 + } return true } -// このコードに問題あり -func (a *Account) Exit(price float64) bool { +// エラーの原因は、空売りの利益計算ロジックの間違いにあった。 +func (a *Account) Exit(positionType string, price float64) bool { if a.PositionSize <= 0 { return false } - a.Balance += a.PositionSize * price + + var pnl float64 + if positionType == "BUY" { + pnl = (price - a.BuyPrice) * a.PositionSize + } else if positionType == "SELL" { + pnl = (a.SellPrice - price) * a.PositionSize + } + + a.Balance += pnl a.PositionSize = 0.0 + a.BuyPrice = 0.0 + a.SellPrice = 0.0 + return true } @@ -51,6 +98,15 @@ func (a *Account) HolderBuy(price, size float64) bool { return true } +func (a *Account) HolderSell(price float64) bool { + if a.PositionSize <= 0 { + return false + } + a.Balance += price * a.PositionSize + a.PositionSize = 0.0 + return true +} + func (a *Account) GetBalance() float64 { return a.Balance }