From f6a64c06b7283e37512dcaaeff44dc873b1c13b9 Mon Sep 17 00:00:00 2001 From: Marc Dahlem Date: Thu, 22 Apr 2021 11:44:36 +0200 Subject: [PATCH 01/12] Added initial ta4j recording exchange adapter --- build.gradle | 2 + bxbot-exchanges/build.gradle | 2 + bxbot-exchanges/pom.xml | 8 + .../bxbot/exchanges/TA4JRecordingAdapter.java | 274 ++++++++++++++++++ .../ta4jhelper/BuyAndSellSignalsToChart.java | 137 +++++++++ .../exchanges/ta4jhelper/GsonBarData.java | 39 +++ .../exchanges/ta4jhelper/GsonBarSeries.java | 34 +++ .../ta4jhelper/JsonBarsSerializer.java | 70 +++++ .../ta4jhelper/TA4JRecordingRule.java | 28 ++ .../Ta4jOptimalTradingStrategy.java | 78 +++++ .../TradePriceRespectingBacktestExecutor.java | 53 ++++ pom.xml | 10 + 12 files changed, 735 insertions(+) create mode 100644 bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/TA4JRecordingAdapter.java create mode 100644 bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/ta4jhelper/BuyAndSellSignalsToChart.java create mode 100644 bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/ta4jhelper/GsonBarData.java create mode 100644 bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/ta4jhelper/GsonBarSeries.java create mode 100644 bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/ta4jhelper/JsonBarsSerializer.java create mode 100644 bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/ta4jhelper/TA4JRecordingRule.java create mode 100644 bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/ta4jhelper/Ta4jOptimalTradingStrategy.java create mode 100644 bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/ta4jhelper/TradePriceRespectingBacktestExecutor.java diff --git a/build.gradle b/build.gradle index e420c4dd8..3d5af7852 100644 --- a/build.gradle +++ b/build.gradle @@ -66,6 +66,8 @@ ext.libraries = [ jjwt : dependencies.create("io.jsonwebtoken:jjwt:0.9.1"), google_guava : dependencies.create("com.google.guava:guava:30.1-jre"), google_gson : dependencies.create("com.google.code.gson:gson:2.8.6"), + ta4j : dependencies.create("org.ta4j:ta4j-core:0.13"), + jfreechart : dependencies.create("org.jfree:jfreechart:1.0.17"), h2 : dependencies.create("com.h2database:h2:1.4.200"), javax_mail_api : dependencies.create("javax.mail:javax.mail-api:" + ext.versions.javaxMailVersion), javax_mail_sun : dependencies.create("com.sun.mail:javax.mail:" + ext.versions.javaxMailVersion), diff --git a/bxbot-exchanges/build.gradle b/bxbot-exchanges/build.gradle index b97fe9d4c..cb6b92fce 100644 --- a/bxbot-exchanges/build.gradle +++ b/bxbot-exchanges/build.gradle @@ -11,6 +11,8 @@ dependencies { compile libraries.google_guava compile libraries.javax_xml_api compile libraries.javax_xml_impl + compile libraries.ta4j + compile libraries.jfreechart testCompile libraries.junit testCompile libraries.powermock_junit diff --git a/bxbot-exchanges/pom.xml b/bxbot-exchanges/pom.xml index f91046811..87363736a 100644 --- a/bxbot-exchanges/pom.xml +++ b/bxbot-exchanges/pom.xml @@ -84,6 +84,14 @@ com.google.guava guava + + org.ta4j + ta4j-core + + + org.jfree + jfreechart + javax.xml.bind jaxb-api diff --git a/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/TA4JRecordingAdapter.java b/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/TA4JRecordingAdapter.java new file mode 100644 index 000000000..16b488c13 --- /dev/null +++ b/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/TA4JRecordingAdapter.java @@ -0,0 +1,274 @@ +package com.gazbert.bxbot.exchanges; + +import com.gazbert.bxbot.exchange.api.ExchangeAdapter; +import com.gazbert.bxbot.exchange.api.ExchangeConfig; +import com.gazbert.bxbot.exchange.api.OtherConfig; +import com.gazbert.bxbot.exchanges.ta4jhelper.*; +import com.gazbert.bxbot.exchanges.trading.api.impl.BalanceInfoImpl; +import com.gazbert.bxbot.exchanges.trading.api.impl.OpenOrderImpl; +import com.gazbert.bxbot.exchanges.trading.api.impl.TickerImpl; +import com.gazbert.bxbot.trading.api.*; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.ta4j.core.*; +import org.ta4j.core.cost.LinearTransactionCostModel; +import org.ta4j.core.tradereport.PerformanceReport; +import org.ta4j.core.tradereport.TradeStatsReport; +import org.ta4j.core.tradereport.TradingStatement; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.*; + +public class TA4JRecordingAdapter extends AbstractExchangeAdapter implements ExchangeAdapter { + private static final Logger LOG = LogManager.getLogger(); + private static final String BUY_FEE_PROPERTY_NAME = "buy-fee"; + private static final String SELL_FEE_PROPERTY_NAME = "sell-fee"; + + + private BigDecimal buyFeePercentage; + private BigDecimal sellFeePercentage; + private BigDecimal sellLimitDistancePercentage; + private String tradingSeriesTradingPath; + + + private BarSeries tradingSeries; + + private static final String counterCurrency = "ZEUR"; + private static final String baseCurrency = "XXRP"; + private static final String marketID = "XRPEUR"; + + private BigDecimal baseCurrencyBalance = BigDecimal.ZERO; + private BigDecimal counterCurrencyBalance = new BigDecimal(100); // simulated starting balance + private OpenOrder currentOpenOrder; + private int currentTick; + private final TA4JRecordingRule sellOrderRule = new TA4JRecordingRule(); + private final TA4JRecordingRule buyOrderRule = new TA4JRecordingRule(); + + @Override + public void init(ExchangeConfig config) { + LOG.info(() -> "About to initialise ta4j recording ExchangeConfig: " + config); + setOtherConfig(config); + loadRecodingSeriesFromJson(); + currentTick = tradingSeries.getBeginIndex() - 1; + } + + private void loadRecodingSeriesFromJson() { + tradingSeries = JsonBarsSerializer.loadSeries(tradingSeriesTradingPath); + if(tradingSeries == null || tradingSeries.isEmpty()) { + throw new IllegalArgumentException("Could not load ta4j series from json '" + tradingSeriesTradingPath + "'"); + } + } + + private void setOtherConfig(ExchangeConfig exchangeConfig) { + final OtherConfig otherConfig = getOtherConfig(exchangeConfig); + + final String buyFeeInConfig = getOtherConfigItem(otherConfig, BUY_FEE_PROPERTY_NAME); + buyFeePercentage = + new BigDecimal(buyFeeInConfig).divide(new BigDecimal("100"), 8, RoundingMode.HALF_UP); + LOG.info(() -> "Buy fee % in BigDecimal format: " + buyFeePercentage); + + final String sellFeeInConfig = getOtherConfigItem(otherConfig, SELL_FEE_PROPERTY_NAME); + sellFeePercentage = + new BigDecimal(sellFeeInConfig).divide(new BigDecimal("100"), 8, RoundingMode.HALF_UP); + LOG.info(() -> "Sell fee % in BigDecimal format: " + sellFeePercentage); + + final String sellLimitDistanceInConfig = getOtherConfigItem(otherConfig, "sell-stop-limit-percentage-distance"); + sellLimitDistancePercentage = + new BigDecimal(sellLimitDistanceInConfig).divide(new BigDecimal("100"), 8, RoundingMode.HALF_UP); + LOG.info(() -> "Sell (stop-limit order) limit distance % in BigDecimal format: " + sellLimitDistancePercentage); + + tradingSeriesTradingPath = getOtherConfigItem(otherConfig, "trading-series-json-path"); + LOG.info(() -> "path to load series json from for recording:" + tradingSeriesTradingPath); + } + + @Override + public String getImplName() { + return "ta4j recording and analyzing adapter"; + } + + @Override + public MarketOrderBook getMarketOrders(String marketId) throws ExchangeNetworkException, TradingApiException { + throw new TradingApiException("get market orders is not implemented", new UnsupportedOperationException()); + } + + @Override + public List getYourOpenOrders(String marketId) throws ExchangeNetworkException, TradingApiException { + LinkedList result = new LinkedList<>(); + if (currentOpenOrder != null) { + result.add(currentOpenOrder); + } + return result; + } + + @Override + public String createOrder(String marketId, OrderType orderType, BigDecimal quantity, BigDecimal price) throws ExchangeNetworkException, TradingApiException { + if (!marketID.equals(marketId)) { + throw new TradingApiException("Market did not match. Expected: "+ marketID+ ", actual: " + marketId); + } + if (currentOpenOrder != null) { + throw new TradingApiException("Can only record/execute one order at a time. Wait for the open order to fulfill"); + } + String newOrderID = "DUMMY_" + orderType + "_ORDER_ID_" + System.currentTimeMillis(); + Date creationDate = Date.from(tradingSeries.getBar(currentTick).getEndTime().toInstant()); + BigDecimal total = price.multiply(quantity); + currentOpenOrder = new OpenOrderImpl(newOrderID, creationDate, marketId, orderType, price, quantity, quantity, total); + checkOpenOrderExecution(); + return newOrderID; + } + + @Override + public boolean cancelOrder(String orderId, String marketId) throws ExchangeNetworkException, TradingApiException { + if (!marketID.equals(marketId)) { + throw new TradingApiException("Market did not match. Expected: "+ marketID+ ", actual: " + marketId); + } + if (currentOpenOrder == null) { + throw new TradingApiException("Tried to cancel a order, but no open order found"); + } + if (!currentOpenOrder.getId().equals(orderId)) { + throw new TradingApiException("Tried to cancel a order, but the order id does not match the current open order. Expected: " + currentOpenOrder.getId() + ", actual: " + orderId); + } + currentOpenOrder = null; + return true; + } + + @Override + public BigDecimal getLatestMarketPrice(String marketId) throws ExchangeNetworkException, TradingApiException { + return (BigDecimal) tradingSeries.getBar(currentTick).getClosePrice().getDelegate(); + } + + @Override + public BalanceInfo getBalanceInfo() throws ExchangeNetworkException, TradingApiException { + HashMap availableBalances = new HashMap<>(); + availableBalances.put(baseCurrency, baseCurrencyBalance); + availableBalances.put(counterCurrency, counterCurrencyBalance); + return new BalanceInfoImpl(availableBalances, new HashMap<>()); + } + + @Override + public Ticker getTicker(String marketId) throws TradingApiException, ExchangeNetworkException { + currentTick++; + LOG.info("Tick increased to '" + currentTick + "'"); + if (currentTick > tradingSeries.getEndIndex()) { + finishRecording(); + return null; + } + + checkOpenOrderExecution(); + + Bar currentBar = tradingSeries.getBar(currentTick); + BigDecimal last = (BigDecimal) currentBar.getClosePrice().getDelegate(); + BigDecimal bid = (BigDecimal) currentBar.getLowPrice().getDelegate(); + BigDecimal ask = (BigDecimal) currentBar.getHighPrice().getDelegate(); + BigDecimal low = (BigDecimal) currentBar.getLowPrice().getDelegate(); + BigDecimal high = (BigDecimal) currentBar.getHighPrice().getDelegate(); + BigDecimal open = (BigDecimal) currentBar.getOpenPrice().getDelegate(); + BigDecimal volume = (BigDecimal) currentBar.getVolume().getDelegate(); + BigDecimal vwap = BigDecimal.ZERO; + Long timestamp = currentBar.getEndTime().toEpochSecond(); + return new TickerImpl(last, bid, ask, low, high, open, volume, vwap, timestamp); + } + + private void checkOpenOrderExecution() throws TradingApiException, ExchangeNetworkException { + if (currentOpenOrder != null) { + switch (currentOpenOrder.getType()) { + case BUY: + checkOpenBuyOrderExecution(); + break; + case SELL: + checkOpenSellOrderExecution(); + break; + default: + throw new TradingApiException("Order type not recognized: " +currentOpenOrder.getType()); + } + } + } + + private void checkOpenSellOrderExecution() throws TradingApiException, ExchangeNetworkException { + BigDecimal currentBidPrice = (BigDecimal)tradingSeries.getBar(currentTick).getLowPrice().getDelegate(); + if (currentBidPrice.compareTo(currentOpenOrder.getPrice()) <= 0) { + LOG.info("SELL: the bid price is below or equal to the stop price --> record sell order execution with the bid price"); + sellOrderRule.addTrigger(currentTick); + BigDecimal orderPrice = currentOpenOrder.getOriginalQuantity().multiply(currentBidPrice); + BigDecimal buyFees = getPercentageOfSellOrderTakenForExchangeFee(marketID).multiply(orderPrice); + BigDecimal netOrderPrice = orderPrice.subtract(buyFees); + counterCurrencyBalance = counterCurrencyBalance.add(netOrderPrice); + baseCurrencyBalance = baseCurrencyBalance.subtract(currentOpenOrder.getOriginalQuantity()); + currentOpenOrder = null; + } + } + + private void checkOpenBuyOrderExecution() throws TradingApiException, ExchangeNetworkException { + BigDecimal currentAskPrice = (BigDecimal)tradingSeries.getBar(currentTick).getHighPrice().getDelegate(); + if (currentAskPrice.compareTo(currentOpenOrder.getPrice()) <=0) { + LOG.info("BUY: the current ask price is below or queal to the limit price --> record buy order execution with the current ask price"); + buyOrderRule.addTrigger(currentTick); + BigDecimal orderPrice = currentOpenOrder.getOriginalQuantity().multiply(currentAskPrice); + BigDecimal buyFees = getPercentageOfBuyOrderTakenForExchangeFee(marketID).multiply(orderPrice); + BigDecimal netOrderPrice = orderPrice.add(buyFees); + counterCurrencyBalance = counterCurrencyBalance.subtract(netOrderPrice); + baseCurrencyBalance = baseCurrencyBalance.add(currentOpenOrder.getOriginalQuantity()); + currentOpenOrder = null; + } + } + + + private void finishRecording() throws TradingApiException, ExchangeNetworkException { + final List strategies = new ArrayList<>(); + Strategy strategy = new BaseStrategy("Recorded ta4j trades", buyOrderRule, sellOrderRule); + strategies.add(strategy); + Ta4jOptimalTradingStrategy optimalTradingStrategy = new Ta4jOptimalTradingStrategy(tradingSeries, getPercentageOfBuyOrderTakenForExchangeFee(marketID), getPercentageOfSellOrderTakenForExchangeFee(marketID)); + strategies.add(optimalTradingStrategy); + + TradePriceRespectingBacktestExecutor backtestExecutor = new TradePriceRespectingBacktestExecutor(tradingSeries, new LinearTransactionCostModel(getPercentageOfBuyOrderTakenForExchangeFee(marketID).doubleValue())); + List statements = backtestExecutor.execute(strategies, tradingSeries.numOf(25), Order.OrderType.BUY); + logReports(statements); + BuyAndSellSignalsToChart.printSeries(tradingSeries, strategy); + BuyAndSellSignalsToChart.printSeries(tradingSeries, optimalTradingStrategy); + throw new TradingApiException("Simulation end finished. Ending balance: " + getBalanceInfo()); + } + + private void logReports(List statements) { + for(TradingStatement statement:statements) { + LOG.info( () -> + "\n######### "+statement.getStrategy().getName()+" #########\n" + + createPerformanceReport(statement) + "\n" + + createTradesReport(statement)+ "\n"+ + "###########################" + ); + } + } + + private String createTradesReport(TradingStatement statement) { + TradeStatsReport tradeStatsReport = statement.getTradeStatsReport(); + return "--------- trade statistics report ---------\n" + + "loss trade count: " + tradeStatsReport.getLossTradeCount() + "\n" + + "profit trade count: " + tradeStatsReport.getProfitTradeCount() + "\n" + + "break even trade count: " + tradeStatsReport.getBreakEvenTradeCount() + "\n" + + "---------------------------"; + } + + private String createPerformanceReport(TradingStatement statement) { + PerformanceReport performanceReport = statement.getPerformanceReport(); + return "--------- performance report ---------\n" + + "total loss: " + performanceReport.getTotalLoss() + "\n" + + "total profit: " + performanceReport.getTotalProfit() + "\n" + + "total profit loss: " + performanceReport.getTotalProfitLoss() + "\n" + + "total profit loss percentage: " + performanceReport.getTotalProfitLossPercentage() + "\n" + + "---------------------------"; + } + + @Override + public BigDecimal getPercentageOfBuyOrderTakenForExchangeFee(String marketId) throws TradingApiException, ExchangeNetworkException { + return buyFeePercentage; + } + + @Override + public BigDecimal getPercentageOfSellOrderTakenForExchangeFee(String marketId) throws TradingApiException, ExchangeNetworkException { + return sellFeePercentage; + } + + public int getMaxIndex() { + return tradingSeries.getEndIndex(); + } +} diff --git a/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/ta4jhelper/BuyAndSellSignalsToChart.java b/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/ta4jhelper/BuyAndSellSignalsToChart.java new file mode 100644 index 000000000..0be62735b --- /dev/null +++ b/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/ta4jhelper/BuyAndSellSignalsToChart.java @@ -0,0 +1,137 @@ +package com.gazbert.bxbot.exchanges.ta4jhelper; + +import org.jfree.chart.ChartFactory; +import org.jfree.chart.ChartPanel; +import org.jfree.chart.JFreeChart; +import org.jfree.chart.axis.DateAxis; +import org.jfree.chart.plot.Marker; +import org.jfree.chart.plot.ValueMarker; +import org.jfree.chart.plot.XYPlot; +import org.jfree.data.time.Second; +import org.jfree.data.time.TimeSeries; +import org.jfree.data.time.TimeSeriesCollection; +import org.jfree.ui.ApplicationFrame; +import org.jfree.ui.RefineryUtilities; +import org.ta4j.core.*; +import org.ta4j.core.indicators.helpers.ClosePriceIndicator; +import org.ta4j.core.indicators.helpers.HighPriceIndicator; +import org.ta4j.core.indicators.helpers.LowPriceIndicator; +import org.ta4j.core.num.Num; + +import java.awt.*; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; + +/** + * This class builds a graphical chart showing the buy/sell signals of a + * strategy. + */ +public class BuyAndSellSignalsToChart { + + /** + * Builds a JFreeChart time series from a Ta4j bar series and an indicator. + * + * @param barSeries the ta4j bar series + * @param indicator the indicator + * @param name the name of the chart time series + * @return the JFreeChart time series + */ + private static TimeSeries buildChartTimeSeries(BarSeries barSeries, Indicator indicator, + String name) { + TimeSeries chartTimeSeries = new TimeSeries(name); + for (int i = 0; i < barSeries.getBarCount(); i++) { + Bar bar = barSeries.getBar(i); + chartTimeSeries.add(new Second(Date.from(bar.getEndTime().toInstant())), + indicator.getValue(i).doubleValue()); + } + return chartTimeSeries; + } + + /** + * Runs a strategy over a bar series and adds the value markers corresponding to + * buy/sell signals to the plot. + * + * @param series the bar series + * @param strategy the trading strategy + * @param plot the plot + */ + private static void addBuySellSignals(BarSeries series, Strategy strategy, XYPlot plot) { + // Running the strategy + BarSeriesManager seriesManager = new BarSeriesManager(series); + List positions = seriesManager.run(strategy).getTrades(); + // Adding markers to plot + for (Trade position : positions) { + // Buy signal + double buySignalBarTime = new Second( + Date.from(series.getBar(position.getEntry().getIndex()).getEndTime().toInstant())) + .getFirstMillisecond(); + Marker buyMarker = new ValueMarker(buySignalBarTime); + buyMarker.setPaint(Color.GREEN); + buyMarker.setLabel("B"); + plot.addDomainMarker(buyMarker); + // Sell signal + double sellSignalBarTime = new Second( + Date.from(series.getBar(position.getExit().getIndex()).getEndTime().toInstant())) + .getFirstMillisecond(); + Marker sellMarker = new ValueMarker(sellSignalBarTime); + sellMarker.setPaint(Color.RED); + sellMarker.setLabel("S"); + plot.addDomainMarker(sellMarker); + } + } + + /** + * Displays a chart in a frame. + * + * @param chart the chart to be displayed + */ + private static void displayChart(JFreeChart chart) { + // Chart panel + ChartPanel panel = new ChartPanel(chart); + panel.setFillZoomRectangle(true); + panel.setMouseWheelEnabled(true); + panel.setPreferredSize(new Dimension(1024, 400)); + // Application frame + ApplicationFrame frame = new ApplicationFrame("Ta4j example - Buy and sell signals to chart"); + frame.setContentPane(panel); + frame.pack(); + RefineryUtilities.centerFrameOnScreen(frame); + frame.setVisible(true); + } + + public static void printSeries(BarSeries series, Strategy strategy) { + System.setProperty("java.awt.headless", "false"); + /* + * Building chart datasets + */ + TimeSeriesCollection dataset = new TimeSeriesCollection(); + dataset.addSeries(buildChartTimeSeries(series, new ClosePriceIndicator(series), "Close")); + dataset.addSeries(buildChartTimeSeries(series, new HighPriceIndicator(series), "Ask")); + dataset.addSeries(buildChartTimeSeries(series, new LowPriceIndicator(series), "Bid")); + + /* + * Creating the chart + */ + JFreeChart chart = ChartFactory.createTimeSeriesChart(strategy.getName(), // title + "Date", // x-axis label + "Price", // y-axis label + dataset, // data + true, // create legend? + true, // generate tooltips? + false // generate URLs? + ); + XYPlot plot = (XYPlot) chart.getPlot(); + DateAxis axis = (DateAxis) plot.getDomainAxis(); + axis.setDateFormatOverride(new SimpleDateFormat("MM-dd HH:mm:ss")); + + /* + * Running the strategy and adding the buy and sell signals to plot + */ + addBuySellSignals(series, strategy, plot); + /* + * Displaying the chart + */ + displayChart(chart); + } +} diff --git a/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/ta4jhelper/GsonBarData.java b/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/ta4jhelper/GsonBarData.java new file mode 100644 index 000000000..8f9dd381e --- /dev/null +++ b/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/ta4jhelper/GsonBarData.java @@ -0,0 +1,39 @@ +package com.gazbert.bxbot.exchanges.ta4jhelper; + +import org.ta4j.core.Bar; +import org.ta4j.core.BaseBarSeries; + +import java.math.BigDecimal; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; + +public class GsonBarData { + private long endTime; + private Number openPrice; + private Number highPrice; + private Number lowPrice; + private Number closePrice; + private Number volume; + private Number amount; + + public static GsonBarData from(Bar bar) { + GsonBarData result = new GsonBarData(); + result.endTime = bar.getEndTime().toInstant().toEpochMilli(); + result.openPrice = bar.getOpenPrice().getDelegate(); + result.highPrice = bar.getHighPrice().getDelegate(); + result.lowPrice = bar.getLowPrice().getDelegate(); + result.closePrice = bar.getClosePrice().getDelegate(); + result.volume = bar.getVolume().getDelegate(); + result.amount = bar.getAmount().getDelegate(); + return result; + } + + public void addTo(BaseBarSeries barSeries) { + Instant endTimeInstant = Instant.ofEpochMilli(endTime); + ZonedDateTime endBarTime = ZonedDateTime.ofInstant(endTimeInstant, ZoneId.systemDefault()); + Number volumeToAdd = volume == null ? BigDecimal.ZERO : volume; + Number amountToAdd = amount == null ? BigDecimal.ZERO : amount; + barSeries.addBar(endBarTime, openPrice, highPrice, lowPrice, closePrice, volumeToAdd, amountToAdd); + } +} diff --git a/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/ta4jhelper/GsonBarSeries.java b/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/ta4jhelper/GsonBarSeries.java new file mode 100644 index 000000000..2ec021136 --- /dev/null +++ b/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/ta4jhelper/GsonBarSeries.java @@ -0,0 +1,34 @@ +package com.gazbert.bxbot.exchanges.ta4jhelper; + +import org.ta4j.core.Bar; +import org.ta4j.core.BarSeries; +import org.ta4j.core.BaseBarSeries; +import org.ta4j.core.BaseBarSeriesBuilder; + +import java.util.LinkedList; +import java.util.List; + +public class GsonBarSeries { + + private String name; + private List ohlc = new LinkedList<>(); + + public static GsonBarSeries from(BarSeries series) { + GsonBarSeries result = new GsonBarSeries(); + result.name = series.getName(); + List barData = series.getBarData(); + for (Bar bar : barData) { + GsonBarData exportableBarData = GsonBarData.from(bar); + result.ohlc.add(exportableBarData); + } + return result; + } + + public BarSeries toBarSeries() { + BaseBarSeries result = new BaseBarSeriesBuilder().withName(this.name).build(); + for (GsonBarData data : ohlc) { + data.addTo(result); + } + return result; + } +} diff --git a/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/ta4jhelper/JsonBarsSerializer.java b/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/ta4jhelper/JsonBarsSerializer.java new file mode 100644 index 000000000..9bd824c98 --- /dev/null +++ b/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/ta4jhelper/JsonBarsSerializer.java @@ -0,0 +1,70 @@ +package com.gazbert.bxbot.exchanges.ta4jhelper; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import org.ta4j.core.BarSeries; + +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class JsonBarsSerializer { + + private static final Logger LOG = Logger.getLogger(JsonBarsSerializer.class.getName()); + private static Map cachedSeries = new HashMap<>(); + + public static void persistSeries(BarSeries series, String filename) { + GsonBarSeries exportableSeries = GsonBarSeries.from(series); + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + FileWriter writer = null; + try { + writer = new FileWriter(filename); + gson.toJson(exportableSeries, writer); + LOG.info("Bar series '" + series.getName() + "' successfully saved to '" + filename + "'"); + } catch (IOException e) { + e.printStackTrace(); + LOG.log(Level.SEVERE, "Unable to store bars in JSON", e); + } finally { + if (writer != null) { + try { + writer.flush(); + writer.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + + public static BarSeries loadSeries(String filename) { + if (cachedSeries.containsKey(filename)) { + return cachedSeries.get(filename).toBarSeries(); + } + Gson gson = new Gson(); + FileReader reader = null; + BarSeries result = null; + try { + reader = new FileReader(filename); + GsonBarSeries loadedSeries = gson.fromJson(reader, GsonBarSeries.class); + cachedSeries.put(filename, loadedSeries); + result = loadedSeries.toBarSeries(); + LOG.info("Bar series '" + result.getName() + "' successfully loaded. #Entries: " + result.getBarCount()); + } catch (FileNotFoundException e) { + e.printStackTrace(); + LOG.log(Level.SEVERE, "Unable to load bars from JSON", e); + } finally { + try { + if (reader != null) + reader.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + return result; + } +} diff --git a/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/ta4jhelper/TA4JRecordingRule.java b/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/ta4jhelper/TA4JRecordingRule.java new file mode 100644 index 000000000..5acc778d2 --- /dev/null +++ b/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/ta4jhelper/TA4JRecordingRule.java @@ -0,0 +1,28 @@ +package com.gazbert.bxbot.exchanges.ta4jhelper; + +import com.gazbert.bxbot.trading.api.TradingApiException; +import org.ta4j.core.TradingRecord; +import org.ta4j.core.trading.rules.AbstractRule; + +import java.util.HashSet; +import java.util.Set; + +public class TA4JRecordingRule extends AbstractRule { + private Set recordedIndeces = new HashSet<>(); + + public void addTrigger(int index) throws TradingApiException { + if(recordedIndeces.contains(index)) { + throw new TradingApiException("Recorded two trades at the same time."); + } + recordedIndeces.add(index); + } + + + + @Override + public boolean isSatisfied(int index, TradingRecord tradingRecord) { + final boolean satisfied = recordedIndeces.contains(index); + traceIsSatisfied(index, satisfied); + return satisfied; + } +} diff --git a/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/ta4jhelper/Ta4jOptimalTradingStrategy.java b/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/ta4jhelper/Ta4jOptimalTradingStrategy.java new file mode 100644 index 000000000..11e941177 --- /dev/null +++ b/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/ta4jhelper/Ta4jOptimalTradingStrategy.java @@ -0,0 +1,78 @@ +package com.gazbert.bxbot.exchanges.ta4jhelper; + +import com.gazbert.bxbot.trading.api.TradingApiException; +import org.ta4j.core.Bar; +import org.ta4j.core.BarSeries; +import org.ta4j.core.BaseStrategy; +import org.ta4j.core.num.Num; + +import java.math.BigDecimal; + +public class Ta4jOptimalTradingStrategy extends BaseStrategy { + private static final TA4JRecordingRule buyRule = new TA4JRecordingRule(); + private static final TA4JRecordingRule sellRule = new TA4JRecordingRule(); + + public Ta4jOptimalTradingStrategy(BarSeries series, BigDecimal buyFee, BigDecimal sellFee) throws TradingApiException { + super("Optimal trading rule", buyRule, sellRule); + this.calculateOptimalTrades(series, series.numOf(buyFee), series.numOf(sellFee)); + } + + private void calculateOptimalTrades(BarSeries series, Num buyFee, Num sellFee) throws TradingApiException { + int lastSeenMinimumIndex = -1; + Num lastSeenMinimum = null; + int lastSeenMaximumIndex = -1; + Num lastSeenMaximum = null; + + for(int index = series.getBeginIndex(); index <= series.getEndIndex(); index++) { + Bar bar = series.getBar(index); + Num askPrice = bar.getHighPrice(); + Num bidPrice = bar.getLowPrice(); + if (lastSeenMinimum == null) { + lastSeenMinimum = askPrice; + lastSeenMinimumIndex = index; + } else { + if (lastSeenMinimum.isGreaterThan(askPrice)) { + createTrade(lastSeenMinimumIndex, lastSeenMinimum, lastSeenMaximumIndex, lastSeenMaximum); + lastSeenMaximum = null; + lastSeenMaximumIndex = -1; + lastSeenMinimum = askPrice; + lastSeenMinimumIndex = index; + } else { + Num buyFees = lastSeenMinimum.multipliedBy(buyFee); + Num minimumPlusFees = lastSeenMinimum.plus(buyFees); + Num currentPriceSellFees = bidPrice.multipliedBy(sellFee); + Num currentPriceMinusFees = bidPrice.minus(currentPriceSellFees); + if(lastSeenMaximum == null) { + if(currentPriceMinusFees.isGreaterThan(minimumPlusFees)) { + lastSeenMaximum = bidPrice; + lastSeenMaximumIndex = index; + } + } else { + if(bidPrice.isGreaterThanOrEqual(lastSeenMaximum)) { + lastSeenMaximum = bidPrice; + lastSeenMaximumIndex = index; + } else { + Num lastMaxPriceSellFees = lastSeenMaximum.multipliedBy(sellFee); + Num lastMaxPriceMinusFees = lastSeenMaximum.minus(lastMaxPriceSellFees); + Num currentPricePlusBuyFees = bidPrice.plus(bidPrice.multipliedBy(buyFee)); + if (currentPricePlusBuyFees.isLessThan(lastMaxPriceMinusFees)) { + createTrade(lastSeenMinimumIndex, lastSeenMinimum, lastSeenMaximumIndex, lastSeenMaximum); + lastSeenMaximum = null; + lastSeenMaximumIndex = -1; + lastSeenMinimum = askPrice; + lastSeenMinimumIndex = index; + } + } + } + } + } + } + } + + private void createTrade(int lastSeenMinimumIndex, Num lastSeenMinimum, int lastSeenMaximumIndex, Num lastSeenMaximum) throws TradingApiException { + if (lastSeenMinimum != null && lastSeenMaximum != null) { + buyRule.addTrigger(lastSeenMinimumIndex); + sellRule.addTrigger(lastSeenMaximumIndex); + } + } +} diff --git a/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/ta4jhelper/TradePriceRespectingBacktestExecutor.java b/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/ta4jhelper/TradePriceRespectingBacktestExecutor.java new file mode 100644 index 000000000..ec5a09179 --- /dev/null +++ b/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/ta4jhelper/TradePriceRespectingBacktestExecutor.java @@ -0,0 +1,53 @@ +package com.gazbert.bxbot.exchanges.ta4jhelper; + +import org.ta4j.core.*; +import org.ta4j.core.cost.CostModel; +import org.ta4j.core.cost.ZeroCostModel; +import org.ta4j.core.num.Num; +import org.ta4j.core.tradereport.TradingStatement; +import org.ta4j.core.tradereport.TradingStatementGenerator; + +import java.util.ArrayList; +import java.util.List; + +public class TradePriceRespectingBacktestExecutor { + + private final TradingStatementGenerator tradingStatementGenerator; + private final BarSeriesManager seriesManager; + + public TradePriceRespectingBacktestExecutor(BarSeries series, CostModel transactionCostModel) { + this(series, new TradingStatementGenerator(), transactionCostModel); + } + + public TradePriceRespectingBacktestExecutor(BarSeries series, TradingStatementGenerator tradingStatementGenerator, CostModel transactionCostModel) { + this.seriesManager = new BarSeriesManager(series, transactionCostModel, new ZeroCostModel()); + this.tradingStatementGenerator = tradingStatementGenerator; + } + + /** + * Execute given strategies and return trading statements + * + * @param amount - The amount used to open/close the trades + */ + public List execute(List strategies, Num amount) { + return execute(strategies, amount, Order.OrderType.BUY); + } + + /** + * Execute given strategies with specified order type to open trades and return + * trading statements + * + * @param amount - The amount used to open/close the trades + * @param orderType the {@link Order.OrderType} used to open the trades + */ + public List execute(List strategies, Num amount, Order.OrderType orderType) { + final List tradingStatements = new ArrayList<>(strategies.size()); + for (Strategy strategy : strategies) { + final TradingRecord tradingRecord = seriesManager.run(strategy, orderType, amount); + final TradingStatement tradingStatement = tradingStatementGenerator.generate(strategy, tradingRecord, + seriesManager.getBarSeries()); + tradingStatements.add(tradingStatement); + } + return tradingStatements; + } +} diff --git a/pom.xml b/pom.xml index 46f581253..11f41af6c 100644 --- a/pom.xml +++ b/pom.xml @@ -276,6 +276,16 @@ hibernate-validator-annotation-processor ${hibernate-vaildator.version} + + org.ta4j + ta4j-core + 0.13 + + + org.jfree + jfreechart + 1.0.17 + record sell order execution with the bid price"); sellOrderRule.addTrigger(currentTick); BigDecimal orderPrice = currentOpenOrder.getOriginalQuantity().multiply(currentBidPrice); - BigDecimal buyFees = getPercentageOfSellOrderTakenForExchangeFee(marketID).multiply(orderPrice); + BigDecimal buyFees = getPercentageOfSellOrderTakenForExchangeFee(marketId).multiply(orderPrice); BigDecimal netOrderPrice = orderPrice.subtract(buyFees); counterCurrencyBalance = counterCurrencyBalance.add(netOrderPrice); baseCurrencyBalance = baseCurrencyBalance.subtract(currentOpenOrder.getOriginalQuantity()); @@ -198,13 +191,13 @@ private void checkOpenSellOrderExecution() throws TradingApiException, ExchangeN } } - private void checkOpenBuyOrderExecution() throws TradingApiException, ExchangeNetworkException { + private void checkOpenBuyOrderExecution(String marketId) throws TradingApiException, ExchangeNetworkException { BigDecimal currentAskPrice = (BigDecimal)tradingSeries.getBar(currentTick).getHighPrice().getDelegate(); if (currentAskPrice.compareTo(currentOpenOrder.getPrice()) <=0) { LOG.info("BUY: the current ask price is below or queal to the limit price --> record buy order execution with the current ask price"); buyOrderRule.addTrigger(currentTick); BigDecimal orderPrice = currentOpenOrder.getOriginalQuantity().multiply(currentAskPrice); - BigDecimal buyFees = getPercentageOfBuyOrderTakenForExchangeFee(marketID).multiply(orderPrice); + BigDecimal buyFees = getPercentageOfBuyOrderTakenForExchangeFee(marketId).multiply(orderPrice); BigDecimal netOrderPrice = orderPrice.add(buyFees); counterCurrencyBalance = counterCurrencyBalance.subtract(netOrderPrice); baseCurrencyBalance = baseCurrencyBalance.add(currentOpenOrder.getOriginalQuantity()); @@ -213,14 +206,14 @@ private void checkOpenBuyOrderExecution() throws TradingApiException, ExchangeNe } - private void finishRecording() throws TradingApiException, ExchangeNetworkException { + private void finishRecording(String marketId) throws TradingApiException, ExchangeNetworkException { final List strategies = new ArrayList<>(); Strategy strategy = new BaseStrategy("Recorded ta4j trades", buyOrderRule, sellOrderRule); strategies.add(strategy); - Ta4jOptimalTradingStrategy optimalTradingStrategy = new Ta4jOptimalTradingStrategy(tradingSeries, getPercentageOfBuyOrderTakenForExchangeFee(marketID), getPercentageOfSellOrderTakenForExchangeFee(marketID)); + Ta4jOptimalTradingStrategy optimalTradingStrategy = new Ta4jOptimalTradingStrategy(tradingSeries, getPercentageOfBuyOrderTakenForExchangeFee(marketId), getPercentageOfSellOrderTakenForExchangeFee(marketId)); strategies.add(optimalTradingStrategy); - TradePriceRespectingBacktestExecutor backtestExecutor = new TradePriceRespectingBacktestExecutor(tradingSeries, new LinearTransactionCostModel(getPercentageOfBuyOrderTakenForExchangeFee(marketID).doubleValue())); + TradePriceRespectingBacktestExecutor backtestExecutor = new TradePriceRespectingBacktestExecutor(tradingSeries, new LinearTransactionCostModel(getPercentageOfBuyOrderTakenForExchangeFee(marketId).doubleValue())); List statements = backtestExecutor.execute(strategies, tradingSeries.numOf(25), Order.OrderType.BUY); logReports(statements); BuyAndSellSignalsToChart.printSeries(tradingSeries, strategy); @@ -267,8 +260,4 @@ public BigDecimal getPercentageOfBuyOrderTakenForExchangeFee(String marketId) th public BigDecimal getPercentageOfSellOrderTakenForExchangeFee(String marketId) throws TradingApiException, ExchangeNetworkException { return sellFeePercentage; } - - public int getMaxIndex() { - return tradingSeries.getEndIndex(); - } } From f5c7ce33f22c8c819cd6bca07548106ca47e70fa Mon Sep 17 00:00:00 2001 From: Marc Dahlem Date: Thu, 22 Apr 2021 12:22:45 +0200 Subject: [PATCH 03/12] Made harded simulation currencies configurable --- .../bxbot/exchanges/TA4JRecordingAdapter.java | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/TA4JRecordingAdapter.java b/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/TA4JRecordingAdapter.java index a3bfdfba2..fd0f4cafc 100644 --- a/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/TA4JRecordingAdapter.java +++ b/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/TA4JRecordingAdapter.java @@ -24,21 +24,24 @@ public class TA4JRecordingAdapter extends AbstractExchangeAdapter implements Exc private static final Logger LOG = LogManager.getLogger(); private static final String BUY_FEE_PROPERTY_NAME = "buy-fee"; private static final String SELL_FEE_PROPERTY_NAME = "sell-fee"; + private static final String SIMULATED_COUNTER_CURRENCY_PROPERTY_NAME = "simulatedCounterCurrency"; + private static final String COUNTER_CURRENCY_START_BALANCE_PROPERTY_NAME = "counterCurrencyStartingBalance"; + private static final String SIMULATED_BASE_CURRENCY_PROPERTY_NAME = "simulatedBaseCurrency"; private BigDecimal buyFeePercentage; private BigDecimal sellFeePercentage; private BigDecimal sellLimitDistancePercentage; private String tradingSeriesTradingPath; - + private String simulatedCounterCurrency; + private String simulatedBaseCurrency; private BarSeries tradingSeries; - private static final String counterCurrency = "ZEUR"; - private static final String baseCurrency = "XXRP"; + private BigDecimal baseCurrencyBalance = BigDecimal.ZERO; - private BigDecimal counterCurrencyBalance = new BigDecimal(100); // simulated starting balance + private BigDecimal counterCurrencyBalance; private OpenOrder currentOpenOrder; private int currentTick; private final TA4JRecordingRule sellOrderRule = new TA4JRecordingRule(); @@ -79,6 +82,16 @@ private void setOtherConfig(ExchangeConfig exchangeConfig) { tradingSeriesTradingPath = getOtherConfigItem(otherConfig, "trading-series-json-path"); LOG.info(() -> "path to load series json from for recording:" + tradingSeriesTradingPath); + + simulatedBaseCurrency = getOtherConfigItem(otherConfig, SIMULATED_BASE_CURRENCY_PROPERTY_NAME); + LOG.info(() -> "Base currency to be simulated:" + simulatedBaseCurrency); + + simulatedCounterCurrency = getOtherConfigItem(otherConfig, SIMULATED_COUNTER_CURRENCY_PROPERTY_NAME); + LOG.info(() -> "Counter currency to be simulated:" + simulatedCounterCurrency); + + final String startingBalanceInConfig = getOtherConfigItem(otherConfig, COUNTER_CURRENCY_START_BALANCE_PROPERTY_NAME); + counterCurrencyBalance = new BigDecimal(startingBalanceInConfig); + LOG.info(() -> "Counter currency balance at simulation start in BigDecimal format: " + counterCurrencyBalance); } @Override @@ -133,8 +146,8 @@ public BigDecimal getLatestMarketPrice(String marketId) throws ExchangeNetworkEx @Override public BalanceInfo getBalanceInfo() throws ExchangeNetworkException, TradingApiException { HashMap availableBalances = new HashMap<>(); - availableBalances.put(baseCurrency, baseCurrencyBalance); - availableBalances.put(counterCurrency, counterCurrencyBalance); + availableBalances.put(simulatedBaseCurrency, baseCurrencyBalance); + availableBalances.put(simulatedCounterCurrency, counterCurrencyBalance); return new BalanceInfoImpl(availableBalances, new HashMap<>()); } From 3dae22f79cfd5ba69d22b0c481b0c013b6d4d7f0 Mon Sep 17 00:00:00 2001 From: Marc Dahlem Date: Thu, 22 Apr 2021 12:48:34 +0200 Subject: [PATCH 04/12] Changed sell from stop order to limit order --- .../bxbot/exchanges/TA4JRecordingAdapter.java | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/TA4JRecordingAdapter.java b/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/TA4JRecordingAdapter.java index fd0f4cafc..e2cceac45 100644 --- a/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/TA4JRecordingAdapter.java +++ b/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/TA4JRecordingAdapter.java @@ -27,6 +27,9 @@ public class TA4JRecordingAdapter extends AbstractExchangeAdapter implements Exc private static final String SIMULATED_COUNTER_CURRENCY_PROPERTY_NAME = "simulatedCounterCurrency"; private static final String COUNTER_CURRENCY_START_BALANCE_PROPERTY_NAME = "counterCurrencyStartingBalance"; private static final String SIMULATED_BASE_CURRENCY_PROPERTY_NAME = "simulatedBaseCurrency"; + private static final String PATH_TO_SERIES_JSON_PROPERTY_NAME = "trading-series-json-path"; + private static final String SHOULD_GENERATE_CHARTS_PROPERTY_NAME = "generate-order-overview-charts"; + private BigDecimal buyFeePercentage; @@ -35,11 +38,10 @@ public class TA4JRecordingAdapter extends AbstractExchangeAdapter implements Exc private String tradingSeriesTradingPath; private String simulatedCounterCurrency; private String simulatedBaseCurrency; + private boolean shouldPrintCharts; private BarSeries tradingSeries; - - private BigDecimal baseCurrencyBalance = BigDecimal.ZERO; private BigDecimal counterCurrencyBalance; private OpenOrder currentOpenOrder; @@ -47,6 +49,7 @@ public class TA4JRecordingAdapter extends AbstractExchangeAdapter implements Exc private final TA4JRecordingRule sellOrderRule = new TA4JRecordingRule(); private final TA4JRecordingRule buyOrderRule = new TA4JRecordingRule(); + @Override public void init(ExchangeConfig config) { LOG.info(() -> "About to initialise ta4j recording ExchangeConfig: " + config); @@ -80,7 +83,7 @@ private void setOtherConfig(ExchangeConfig exchangeConfig) { new BigDecimal(sellLimitDistanceInConfig).divide(new BigDecimal("100"), 8, RoundingMode.HALF_UP); LOG.info(() -> "Sell (stop-limit order) limit distance % in BigDecimal format: " + sellLimitDistancePercentage); - tradingSeriesTradingPath = getOtherConfigItem(otherConfig, "trading-series-json-path"); + tradingSeriesTradingPath = getOtherConfigItem(otherConfig, PATH_TO_SERIES_JSON_PROPERTY_NAME); LOG.info(() -> "path to load series json from for recording:" + tradingSeriesTradingPath); simulatedBaseCurrency = getOtherConfigItem(otherConfig, SIMULATED_BASE_CURRENCY_PROPERTY_NAME); @@ -92,6 +95,11 @@ private void setOtherConfig(ExchangeConfig exchangeConfig) { final String startingBalanceInConfig = getOtherConfigItem(otherConfig, COUNTER_CURRENCY_START_BALANCE_PROPERTY_NAME); counterCurrencyBalance = new BigDecimal(startingBalanceInConfig); LOG.info(() -> "Counter currency balance at simulation start in BigDecimal format: " + counterCurrencyBalance); + + final String shouldGenerateChartsInConfig = getOtherConfigItem(otherConfig, SHOULD_GENERATE_CHARTS_PROPERTY_NAME); + shouldPrintCharts = Boolean.parseBoolean(shouldGenerateChartsInConfig); + LOG.info(() -> "Should print charts at simulation end: " + shouldPrintCharts); + } @Override @@ -164,8 +172,8 @@ public Ticker getTicker(String marketId) throws TradingApiException, ExchangeNet Bar currentBar = tradingSeries.getBar(currentTick); BigDecimal last = (BigDecimal) currentBar.getClosePrice().getDelegate(); - BigDecimal bid = (BigDecimal) currentBar.getLowPrice().getDelegate(); - BigDecimal ask = (BigDecimal) currentBar.getHighPrice().getDelegate(); + BigDecimal bid = (BigDecimal) currentBar.getLowPrice().getDelegate(); // assumes that the stored series json contains the bid price in the low price property + BigDecimal ask = (BigDecimal) currentBar.getHighPrice().getDelegate(); // assumes that the stored series json contains the ask price in the high price property BigDecimal low = (BigDecimal) currentBar.getLowPrice().getDelegate(); BigDecimal high = (BigDecimal) currentBar.getHighPrice().getDelegate(); BigDecimal open = (BigDecimal) currentBar.getOpenPrice().getDelegate(); @@ -191,9 +199,9 @@ private void checkOpenOrderExecution(String marketId) throws TradingApiException } private void checkOpenSellOrderExecution(String marketId) throws TradingApiException, ExchangeNetworkException { - BigDecimal currentBidPrice = (BigDecimal)tradingSeries.getBar(currentTick).getLowPrice().getDelegate(); - if (currentBidPrice.compareTo(currentOpenOrder.getPrice()) <= 0) { - LOG.info("SELL: the bid price is below or equal to the stop price --> record sell order execution with the bid price"); + BigDecimal currentBidPrice = (BigDecimal)tradingSeries.getBar(currentTick).getLowPrice().getDelegate(); // assumes that the stored series json contains the bid price in the low price property + if (currentBidPrice.compareTo(currentOpenOrder.getPrice()) >= 0) { + LOG.info("SELL: the market's bid price moved above the limit price --> record sell order execution with the current bid price"); sellOrderRule.addTrigger(currentTick); BigDecimal orderPrice = currentOpenOrder.getOriginalQuantity().multiply(currentBidPrice); BigDecimal buyFees = getPercentageOfSellOrderTakenForExchangeFee(marketId).multiply(orderPrice); @@ -205,9 +213,9 @@ private void checkOpenSellOrderExecution(String marketId) throws TradingApiExcep } private void checkOpenBuyOrderExecution(String marketId) throws TradingApiException, ExchangeNetworkException { - BigDecimal currentAskPrice = (BigDecimal)tradingSeries.getBar(currentTick).getHighPrice().getDelegate(); + BigDecimal currentAskPrice = (BigDecimal)tradingSeries.getBar(currentTick).getHighPrice().getDelegate(); // assumes that the stored series json contains the ask price in the high price property if (currentAskPrice.compareTo(currentOpenOrder.getPrice()) <=0) { - LOG.info("BUY: the current ask price is below or queal to the limit price --> record buy order execution with the current ask price"); + LOG.info("BUY: the market's current ask price moved below the limit price --> record buy order execution with the current ask price"); buyOrderRule.addTrigger(currentTick); BigDecimal orderPrice = currentOpenOrder.getOriginalQuantity().multiply(currentAskPrice); BigDecimal buyFees = getPercentageOfBuyOrderTakenForExchangeFee(marketId).multiply(orderPrice); @@ -223,14 +231,17 @@ private void finishRecording(String marketId) throws TradingApiException, Exchan final List strategies = new ArrayList<>(); Strategy strategy = new BaseStrategy("Recorded ta4j trades", buyOrderRule, sellOrderRule); strategies.add(strategy); + Ta4jOptimalTradingStrategy optimalTradingStrategy = new Ta4jOptimalTradingStrategy(tradingSeries, getPercentageOfBuyOrderTakenForExchangeFee(marketId), getPercentageOfSellOrderTakenForExchangeFee(marketId)); strategies.add(optimalTradingStrategy); TradePriceRespectingBacktestExecutor backtestExecutor = new TradePriceRespectingBacktestExecutor(tradingSeries, new LinearTransactionCostModel(getPercentageOfBuyOrderTakenForExchangeFee(marketId).doubleValue())); List statements = backtestExecutor.execute(strategies, tradingSeries.numOf(25), Order.OrderType.BUY); logReports(statements); - BuyAndSellSignalsToChart.printSeries(tradingSeries, strategy); - BuyAndSellSignalsToChart.printSeries(tradingSeries, optimalTradingStrategy); + if(shouldPrintCharts) { + BuyAndSellSignalsToChart.printSeries(tradingSeries, strategy); + BuyAndSellSignalsToChart.printSeries(tradingSeries, optimalTradingStrategy); + } throw new TradingApiException("Simulation end finished. Ending balance: " + getBalanceInfo()); } From de1bdd5746b3f69952b03a8d63435b1a230d157d Mon Sep 17 00:00:00 2001 From: Marc Dahlem Date: Thu, 22 Apr 2021 12:50:24 +0200 Subject: [PATCH 05/12] Removed limit distance for stop-limit orders --- .../com/gazbert/bxbot/exchanges/TA4JRecordingAdapter.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/TA4JRecordingAdapter.java b/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/TA4JRecordingAdapter.java index e2cceac45..db1bc4f16 100644 --- a/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/TA4JRecordingAdapter.java +++ b/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/TA4JRecordingAdapter.java @@ -34,7 +34,6 @@ public class TA4JRecordingAdapter extends AbstractExchangeAdapter implements Exc private BigDecimal buyFeePercentage; private BigDecimal sellFeePercentage; - private BigDecimal sellLimitDistancePercentage; private String tradingSeriesTradingPath; private String simulatedCounterCurrency; private String simulatedBaseCurrency; @@ -78,11 +77,6 @@ private void setOtherConfig(ExchangeConfig exchangeConfig) { new BigDecimal(sellFeeInConfig).divide(new BigDecimal("100"), 8, RoundingMode.HALF_UP); LOG.info(() -> "Sell fee % in BigDecimal format: " + sellFeePercentage); - final String sellLimitDistanceInConfig = getOtherConfigItem(otherConfig, "sell-stop-limit-percentage-distance"); - sellLimitDistancePercentage = - new BigDecimal(sellLimitDistanceInConfig).divide(new BigDecimal("100"), 8, RoundingMode.HALF_UP); - LOG.info(() -> "Sell (stop-limit order) limit distance % in BigDecimal format: " + sellLimitDistancePercentage); - tradingSeriesTradingPath = getOtherConfigItem(otherConfig, PATH_TO_SERIES_JSON_PROPERTY_NAME); LOG.info(() -> "path to load series json from for recording:" + tradingSeriesTradingPath); From 2240b4660669000212c6c20f2f3dcabf11af555c Mon Sep 17 00:00:00 2001 From: Marc Dahlem Date: Thu, 22 Apr 2021 13:09:36 +0200 Subject: [PATCH 06/12] Removed differentation between buy and sell orders --- .../bxbot/exchanges/TA4JRecordingAdapter.java | 23 +++++++------------ 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/TA4JRecordingAdapter.java b/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/TA4JRecordingAdapter.java index db1bc4f16..0380b3d3f 100644 --- a/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/TA4JRecordingAdapter.java +++ b/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/TA4JRecordingAdapter.java @@ -22,8 +22,7 @@ public class TA4JRecordingAdapter extends AbstractExchangeAdapter implements ExchangeAdapter { private static final Logger LOG = LogManager.getLogger(); - private static final String BUY_FEE_PROPERTY_NAME = "buy-fee"; - private static final String SELL_FEE_PROPERTY_NAME = "sell-fee"; + private static final String ORDER_FEE_PROPERTY_NAME = "order-fee"; private static final String SIMULATED_COUNTER_CURRENCY_PROPERTY_NAME = "simulatedCounterCurrency"; private static final String COUNTER_CURRENCY_START_BALANCE_PROPERTY_NAME = "counterCurrencyStartingBalance"; private static final String SIMULATED_BASE_CURRENCY_PROPERTY_NAME = "simulatedBaseCurrency"; @@ -32,8 +31,7 @@ public class TA4JRecordingAdapter extends AbstractExchangeAdapter implements Exc - private BigDecimal buyFeePercentage; - private BigDecimal sellFeePercentage; + private BigDecimal orderFeePercentage; private String tradingSeriesTradingPath; private String simulatedCounterCurrency; private String simulatedBaseCurrency; @@ -67,15 +65,10 @@ private void loadRecodingSeriesFromJson() { private void setOtherConfig(ExchangeConfig exchangeConfig) { final OtherConfig otherConfig = getOtherConfig(exchangeConfig); - final String buyFeeInConfig = getOtherConfigItem(otherConfig, BUY_FEE_PROPERTY_NAME); - buyFeePercentage = - new BigDecimal(buyFeeInConfig).divide(new BigDecimal("100"), 8, RoundingMode.HALF_UP); - LOG.info(() -> "Buy fee % in BigDecimal format: " + buyFeePercentage); - - final String sellFeeInConfig = getOtherConfigItem(otherConfig, SELL_FEE_PROPERTY_NAME); - sellFeePercentage = - new BigDecimal(sellFeeInConfig).divide(new BigDecimal("100"), 8, RoundingMode.HALF_UP); - LOG.info(() -> "Sell fee % in BigDecimal format: " + sellFeePercentage); + final String orderFeeInConfig = getOtherConfigItem(otherConfig, ORDER_FEE_PROPERTY_NAME); + orderFeePercentage = + new BigDecimal(orderFeeInConfig).divide(new BigDecimal("100"), 8, RoundingMode.HALF_UP); + LOG.info(() -> "Order fee % in BigDecimal format: " + orderFeePercentage); tradingSeriesTradingPath = getOtherConfigItem(otherConfig, PATH_TO_SERIES_JSON_PROPERTY_NAME); LOG.info(() -> "path to load series json from for recording:" + tradingSeriesTradingPath); @@ -271,11 +264,11 @@ private String createPerformanceReport(TradingStatement statement) { @Override public BigDecimal getPercentageOfBuyOrderTakenForExchangeFee(String marketId) throws TradingApiException, ExchangeNetworkException { - return buyFeePercentage; + return orderFeePercentage; } @Override public BigDecimal getPercentageOfSellOrderTakenForExchangeFee(String marketId) throws TradingApiException, ExchangeNetworkException { - return sellFeePercentage; + return orderFeePercentage; } } From 05f087d7f55382666e5be9edf0ee72de18ca9fdd Mon Sep 17 00:00:00 2001 From: Marc Dahlem Date: Thu, 22 Apr 2021 13:17:42 +0200 Subject: [PATCH 07/12] Code format --- .../bxbot/exchanges/TA4JRecordingAdapter.java | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/TA4JRecordingAdapter.java b/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/TA4JRecordingAdapter.java index 0380b3d3f..128b6029f 100644 --- a/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/TA4JRecordingAdapter.java +++ b/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/TA4JRecordingAdapter.java @@ -30,7 +30,6 @@ public class TA4JRecordingAdapter extends AbstractExchangeAdapter implements Exc private static final String SHOULD_GENERATE_CHARTS_PROPERTY_NAME = "generate-order-overview-charts"; - private BigDecimal orderFeePercentage; private String tradingSeriesTradingPath; private String simulatedCounterCurrency; @@ -57,7 +56,7 @@ public void init(ExchangeConfig config) { private void loadRecodingSeriesFromJson() { tradingSeries = JsonBarsSerializer.loadSeries(tradingSeriesTradingPath); - if(tradingSeries == null || tradingSeries.isEmpty()) { + if (tradingSeries == null || tradingSeries.isEmpty()) { throw new IllegalArgumentException("Could not load ta4j series from json '" + tradingSeriesTradingPath + "'"); } } @@ -180,13 +179,13 @@ private void checkOpenOrderExecution(String marketId) throws TradingApiException checkOpenSellOrderExecution(marketId); break; default: - throw new TradingApiException("Order type not recognized: " +currentOpenOrder.getType()); + throw new TradingApiException("Order type not recognized: " + currentOpenOrder.getType()); } } } private void checkOpenSellOrderExecution(String marketId) throws TradingApiException, ExchangeNetworkException { - BigDecimal currentBidPrice = (BigDecimal)tradingSeries.getBar(currentTick).getLowPrice().getDelegate(); // assumes that the stored series json contains the bid price in the low price property + BigDecimal currentBidPrice = (BigDecimal) tradingSeries.getBar(currentTick).getLowPrice().getDelegate(); // assumes that the stored series json contains the bid price in the low price property if (currentBidPrice.compareTo(currentOpenOrder.getPrice()) >= 0) { LOG.info("SELL: the market's bid price moved above the limit price --> record sell order execution with the current bid price"); sellOrderRule.addTrigger(currentTick); @@ -200,8 +199,8 @@ private void checkOpenSellOrderExecution(String marketId) throws TradingApiExcep } private void checkOpenBuyOrderExecution(String marketId) throws TradingApiException, ExchangeNetworkException { - BigDecimal currentAskPrice = (BigDecimal)tradingSeries.getBar(currentTick).getHighPrice().getDelegate(); // assumes that the stored series json contains the ask price in the high price property - if (currentAskPrice.compareTo(currentOpenOrder.getPrice()) <=0) { + BigDecimal currentAskPrice = (BigDecimal) tradingSeries.getBar(currentTick).getHighPrice().getDelegate(); // assumes that the stored series json contains the ask price in the high price property + if (currentAskPrice.compareTo(currentOpenOrder.getPrice()) <= 0) { LOG.info("BUY: the market's current ask price moved below the limit price --> record buy order execution with the current ask price"); buyOrderRule.addTrigger(currentTick); BigDecimal orderPrice = currentOpenOrder.getOriginalQuantity().multiply(currentAskPrice); @@ -225,7 +224,7 @@ private void finishRecording(String marketId) throws TradingApiException, Exchan TradePriceRespectingBacktestExecutor backtestExecutor = new TradePriceRespectingBacktestExecutor(tradingSeries, new LinearTransactionCostModel(getPercentageOfBuyOrderTakenForExchangeFee(marketId).doubleValue())); List statements = backtestExecutor.execute(strategies, tradingSeries.numOf(25), Order.OrderType.BUY); logReports(statements); - if(shouldPrintCharts) { + if (shouldPrintCharts) { BuyAndSellSignalsToChart.printSeries(tradingSeries, strategy); BuyAndSellSignalsToChart.printSeries(tradingSeries, optimalTradingStrategy); } @@ -233,12 +232,12 @@ private void finishRecording(String marketId) throws TradingApiException, Exchan } private void logReports(List statements) { - for(TradingStatement statement:statements) { - LOG.info( () -> - "\n######### "+statement.getStrategy().getName()+" #########\n" + - createPerformanceReport(statement) + "\n" + - createTradesReport(statement)+ "\n"+ - "###########################" + for (TradingStatement statement : statements) { + LOG.info(() -> + "\n######### " + statement.getStrategy().getName() + " #########\n" + + createPerformanceReport(statement) + "\n" + + createTradesReport(statement) + "\n" + + "###########################" ); } } From f2ee05f9dd9ee02f1e874c2a6814d1eeb395356e Mon Sep 17 00:00:00 2001 From: Marc Dahlem Date: Thu, 22 Apr 2021 23:51:19 +0200 Subject: [PATCH 08/12] Added ta4j example strategy for recording live market data to a json --- bxbot-strategies/build.gradle | 2 + bxbot-strategies/pom.xml | 8 +++ .../ExampleTA4JRecordingStrategy.java | 63 +++++++++++++++++ .../strategies/ta4jhelper/GsonBarData.java | 39 +++++++++++ .../strategies/ta4jhelper/GsonBarSeries.java | 34 +++++++++ .../ta4jhelper/JsonBarsSerializer.java | 70 +++++++++++++++++++ 6 files changed, 216 insertions(+) create mode 100644 bxbot-strategies/src/main/java/com/gazbert/bxbot/strategies/ExampleTA4JRecordingStrategy.java create mode 100644 bxbot-strategies/src/main/java/com/gazbert/bxbot/strategies/ta4jhelper/GsonBarData.java create mode 100644 bxbot-strategies/src/main/java/com/gazbert/bxbot/strategies/ta4jhelper/GsonBarSeries.java create mode 100644 bxbot-strategies/src/main/java/com/gazbert/bxbot/strategies/ta4jhelper/JsonBarsSerializer.java diff --git a/bxbot-strategies/build.gradle b/bxbot-strategies/build.gradle index cb9dbd268..7f7a798d8 100644 --- a/bxbot-strategies/build.gradle +++ b/bxbot-strategies/build.gradle @@ -7,7 +7,9 @@ dependencies { compile libraries.spring_boot_starter compile libraries.spring_boot_starter_log4j2 + compile libraries.google_gson compile libraries.google_guava + compile libraries.ta4j testCompile libraries.junit testCompile libraries.powermock_junit diff --git a/bxbot-strategies/pom.xml b/bxbot-strategies/pom.xml index 6fc353c22..a5c88f5f0 100644 --- a/bxbot-strategies/pom.xml +++ b/bxbot-strategies/pom.xml @@ -46,10 +46,18 @@ org.springframework.boot spring-boot-starter-log4j2 + + com.google.code.gson + gson + com.google.guava guava + + org.ta4j + ta4j-core + last market price + // * High --> ask market price + // * Low --> bid market price + series.addBar(ZonedDateTime.now(), currentTicker.getLast(), tickHighPrice, tickLowPrice, currentTicker.getLast()); + } catch (TradingApiException | ExchangeNetworkException e) { + // as soon as the server communcation fails, store the recorded series to a json file + String filename = market.getId() + "_" + System.currentTimeMillis() + ".json"; + JsonBarsSerializer.persistSeries(series, filename); + LOG.info(() -> market.getName() + " Stored recorded market data as json to '" + filename + "'"); + + // We are just going to re-throw as StrategyException for engine to deal with - it will + // shutdown the bot. + LOG.error( + market.getName() + + " Failed to perform the strategy because Exchange threw TradingApiException, ExchangeNetworkexception or StrategyException. " + + "Telling Trading Engine to shutdown bot!", + e); + throw new StrategyException(e); + } + } +} diff --git a/bxbot-strategies/src/main/java/com/gazbert/bxbot/strategies/ta4jhelper/GsonBarData.java b/bxbot-strategies/src/main/java/com/gazbert/bxbot/strategies/ta4jhelper/GsonBarData.java new file mode 100644 index 000000000..f6ea13e79 --- /dev/null +++ b/bxbot-strategies/src/main/java/com/gazbert/bxbot/strategies/ta4jhelper/GsonBarData.java @@ -0,0 +1,39 @@ +package com.gazbert.bxbot.strategies.ta4jhelper; + +import org.ta4j.core.Bar; +import org.ta4j.core.BaseBarSeries; + +import java.math.BigDecimal; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; + +public class GsonBarData { + private long endTime; + private Number openPrice; + private Number highPrice; + private Number lowPrice; + private Number closePrice; + private Number volume; + private Number amount; + + public static GsonBarData from(Bar bar) { + GsonBarData result = new GsonBarData(); + result.endTime = bar.getEndTime().toInstant().toEpochMilli(); + result.openPrice = bar.getOpenPrice().getDelegate(); + result.highPrice = bar.getHighPrice().getDelegate(); + result.lowPrice = bar.getLowPrice().getDelegate(); + result.closePrice = bar.getClosePrice().getDelegate(); + result.volume = bar.getVolume().getDelegate(); + result.amount = bar.getAmount().getDelegate(); + return result; + } + + public void addTo(BaseBarSeries barSeries) { + Instant endTimeInstant = Instant.ofEpochMilli(endTime); + ZonedDateTime endBarTime = ZonedDateTime.ofInstant(endTimeInstant, ZoneId.systemDefault()); + Number volumeToAdd = volume == null ? BigDecimal.ZERO : volume; + Number amountToAdd = amount == null ? BigDecimal.ZERO : amount; + barSeries.addBar(endBarTime, openPrice, highPrice, lowPrice, closePrice, volumeToAdd, amountToAdd); + } +} diff --git a/bxbot-strategies/src/main/java/com/gazbert/bxbot/strategies/ta4jhelper/GsonBarSeries.java b/bxbot-strategies/src/main/java/com/gazbert/bxbot/strategies/ta4jhelper/GsonBarSeries.java new file mode 100644 index 000000000..cc676cadb --- /dev/null +++ b/bxbot-strategies/src/main/java/com/gazbert/bxbot/strategies/ta4jhelper/GsonBarSeries.java @@ -0,0 +1,34 @@ +package com.gazbert.bxbot.strategies.ta4jhelper; + +import org.ta4j.core.Bar; +import org.ta4j.core.BarSeries; +import org.ta4j.core.BaseBarSeries; +import org.ta4j.core.BaseBarSeriesBuilder; + +import java.util.LinkedList; +import java.util.List; + +public class GsonBarSeries { + + private String name; + private List ohlc = new LinkedList<>(); + + public static GsonBarSeries from(BarSeries series) { + GsonBarSeries result = new GsonBarSeries(); + result.name = series.getName(); + List barData = series.getBarData(); + for (Bar bar : barData) { + GsonBarData exportableBarData = GsonBarData.from(bar); + result.ohlc.add(exportableBarData); + } + return result; + } + + public BarSeries toBarSeries() { + BaseBarSeries result = new BaseBarSeriesBuilder().withName(this.name).build(); + for (GsonBarData data : ohlc) { + data.addTo(result); + } + return result; + } +} diff --git a/bxbot-strategies/src/main/java/com/gazbert/bxbot/strategies/ta4jhelper/JsonBarsSerializer.java b/bxbot-strategies/src/main/java/com/gazbert/bxbot/strategies/ta4jhelper/JsonBarsSerializer.java new file mode 100644 index 000000000..9a215750d --- /dev/null +++ b/bxbot-strategies/src/main/java/com/gazbert/bxbot/strategies/ta4jhelper/JsonBarsSerializer.java @@ -0,0 +1,70 @@ +package com.gazbert.bxbot.strategies.ta4jhelper; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import org.ta4j.core.BarSeries; + +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class JsonBarsSerializer { + + private static final Logger LOG = Logger.getLogger(JsonBarsSerializer.class.getName()); + private static Map cachedSeries = new HashMap<>(); + + public static void persistSeries(BarSeries series, String filename) { + GsonBarSeries exportableSeries = GsonBarSeries.from(series); + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + FileWriter writer = null; + try { + writer = new FileWriter(filename); + gson.toJson(exportableSeries, writer); + LOG.info("Bar series '" + series.getName() + "' successfully saved to '" + filename + "'"); + } catch (IOException e) { + e.printStackTrace(); + LOG.log(Level.SEVERE, "Unable to store bars in JSON", e); + } finally { + if (writer != null) { + try { + writer.flush(); + writer.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + + public static BarSeries loadSeries(String filename) { + if (cachedSeries.containsKey(filename)) { + return cachedSeries.get(filename).toBarSeries(); + } + Gson gson = new Gson(); + FileReader reader = null; + BarSeries result = null; + try { + reader = new FileReader(filename); + GsonBarSeries loadedSeries = gson.fromJson(reader, GsonBarSeries.class); + cachedSeries.put(filename, loadedSeries); + result = loadedSeries.toBarSeries(); + LOG.info("Bar series '" + result.getName() + "' successfully loaded. #Entries: " + result.getBarCount()); + } catch (FileNotFoundException e) { + e.printStackTrace(); + LOG.log(Level.SEVERE, "Unable to load bars from JSON", e); + } finally { + try { + if (reader != null) + reader.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + return result; + } +} From 1f72e2dcb13d5bd88915a57194600896b7cf5f67 Mon Sep 17 00:00:00 2001 From: Marc Dahlem Date: Fri, 23 Apr 2021 00:43:23 +0200 Subject: [PATCH 09/12] Added example ta4j backtest strategy --- .../ExampleTA4JBacktestStrategy.java | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 bxbot-strategies/src/main/java/com/gazbert/bxbot/strategies/ExampleTA4JBacktestStrategy.java diff --git a/bxbot-strategies/src/main/java/com/gazbert/bxbot/strategies/ExampleTA4JBacktestStrategy.java b/bxbot-strategies/src/main/java/com/gazbert/bxbot/strategies/ExampleTA4JBacktestStrategy.java new file mode 100644 index 000000000..365430b81 --- /dev/null +++ b/bxbot-strategies/src/main/java/com/gazbert/bxbot/strategies/ExampleTA4JBacktestStrategy.java @@ -0,0 +1,136 @@ +package com.gazbert.bxbot.strategies; + +import com.gazbert.bxbot.strategies.ta4jhelper.JsonBarsSerializer; +import com.gazbert.bxbot.strategy.api.StrategyConfig; +import com.gazbert.bxbot.strategy.api.StrategyException; +import com.gazbert.bxbot.strategy.api.TradingStrategy; +import com.gazbert.bxbot.trading.api.*; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.stereotype.Component; +import org.ta4j.core.BarSeries; +import org.ta4j.core.BaseBarSeriesBuilder; +import org.ta4j.core.BaseStrategy; +import org.ta4j.core.Rule; +import org.ta4j.core.indicators.SMAIndicator; +import org.ta4j.core.indicators.helpers.ClosePriceIndicator; +import org.ta4j.core.trading.rules.CrossedDownIndicatorRule; +import org.ta4j.core.trading.rules.CrossedUpIndicatorRule; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.DecimalFormat; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; + +@Component("exampleTa4jBacktestStrategy") // used to load the strategy using Spring bean injection +public class ExampleTA4JBacktestStrategy implements TradingStrategy { + + private static final Logger LOG = LogManager.getLogger(); + /** + * The decimal format for the logs. + */ + private static final DecimalFormat decimalFormat = new DecimalFormat("#.########"); + + private TradingApi tradingApi; + private Market market; + private BarSeries series; + private Ticker currentTicker; + private BaseStrategy ta4jStrategy; + + @Override + public void init(TradingApi tradingApi, Market market, StrategyConfig config) { + LOG.info(() -> "Initialising TA4J Backtest Strategy..."); + this.tradingApi = tradingApi; + this.market = market; + series = new BaseBarSeriesBuilder().withName(market.getName() + "_" + System.currentTimeMillis()).build(); + initYourStrategy(); + + LOG.info(() -> "Trading Strategy initialised successfully!"); + } + + private void initYourStrategy() { + // In this example, we use a simple sma strategy with the help of ta4j. You can of course implement your own logic instead + ClosePriceIndicator closePriceIndicator = new ClosePriceIndicator(series); + SMAIndicator shortTimeSma = new SMAIndicator(closePriceIndicator, 30); + SMAIndicator longTimeSma = new SMAIndicator(closePriceIndicator, 200); + Rule entryRule = new CrossedUpIndicatorRule(shortTimeSma, longTimeSma); + Rule exitRule = new CrossedDownIndicatorRule(shortTimeSma, longTimeSma); + ta4jStrategy = new BaseStrategy(entryRule, exitRule); + } + + @Override + public void execute() throws StrategyException { + try { + // first get the current market info. This will update the ta4j backtest exchange to the next tick/timeslot + currentTicker = tradingApi.getTicker(market.getId()); + LOG.info(() -> market.getName() + " Updated latest market info: " + currentTicker); + + BigDecimal tickHighPrice = currentTicker.getAsk(); //save ask price as high price. + BigDecimal tickLowPrice = currentTicker.getBid(); //save bid price as low price. + // Store markets data as own bar per strategy execution. Hereby + // * Close == Open --> last market price + // * High --> ask market price + // * Low --> bid market price + ZonedDateTime tickTime = ZonedDateTime.ofInstant(Instant.ofEpochMilli(currentTicker.getTimestamp()), ZoneId.systemDefault()); + series.addBar(tickTime, currentTicker.getLast(), tickHighPrice, tickLowPrice, currentTicker.getLast()); + + executeStrategy(); + } catch (TradingApiException | ExchangeNetworkException e) { + // We are just going to re-throw as StrategyException for engine to deal with - it will + // shutdown the bot. + LOG.error( + market.getName() + + " Failed to perform the strategy because Exchange threw TradingApiException, ExchangeNetworkexception or StrategyException. " + + "Telling Trading Engine to shutdown bot!", + e); + throw new StrategyException(e); + } + } + + private void executeStrategy() throws ExchangeNetworkException, TradingApiException, StrategyException { + // Ask the ta4j strategy how we want to proceed + int endIndex = series.getEndIndex(); + if (ta4jStrategy.shouldEnter(endIndex)) { + // we should enter the market + shouldEnter(); + } else if (ta4jStrategy.shouldExit(endIndex)) { + // we should leave the market + shouldExit(); + } + } + + private void shouldExit() throws ExchangeNetworkException, TradingApiException { + //place a sell order with the available base currency units with the current bid price to get the order filled directly + BigDecimal availableBaseCurrency = getAvailableCurrencyBalance(market.getBaseCurrency()); + String orderId = tradingApi.createOrder(market.getId(), OrderType.SELL, availableBaseCurrency, currentTicker.getBid()); + LOG.info(() -> market.getName() + " SELL Order sent successfully to exchange. ID: " + orderId); + } + + private void shouldEnter() throws ExchangeNetworkException, TradingApiException { + //place a buy order of 25% of the available counterCurrency units with the current ask price to get the order filled directly + BigDecimal availableCounterCurrency = getAvailableCurrencyBalance(market.getCounterCurrency()); + BigDecimal balanceToUse = availableCounterCurrency.multiply(new BigDecimal("0.25")); + final BigDecimal piecesToBuy = balanceToUse.divide(currentTicker.getAsk(), 8, RoundingMode.HALF_DOWN); + String orderID = tradingApi.createOrder(market.getId(), OrderType.BUY, piecesToBuy, currentTicker.getAsk()); + LOG.info(() -> market.getName() + " BUY Order sent successfully to exchange. ID: " + orderID); + } + + private BigDecimal getAvailableCurrencyBalance(String currency) throws ExchangeNetworkException, TradingApiException { + LOG.info(() -> market.getName() + " Fetching the available balance for the currency '" + currency + "'."); + BalanceInfo balanceInfo = tradingApi.getBalanceInfo(); + final BigDecimal currentBalance = balanceInfo.getBalancesAvailable().get(currency); + if (currentBalance == null) { + final String errorMsg = "Failed to get current currency balance as '" + currency + "' key is not available in the balances map. Balances returned: " + balanceInfo.getBalancesAvailable(); + LOG.warn(() -> errorMsg); + return BigDecimal.ZERO; + } else { + LOG.info(() -> market.getName() + "Currency balance available on exchange is [" + + decimalFormat.format(currentBalance) + + "] " + + currency); + } + return currentBalance; + } +} From 5c89646ca8e0adf5b15dc7aecffb0c20b6f7bfa3 Mon Sep 17 00:00:00 2001 From: Marc Dahlem Date: Fri, 23 Apr 2021 00:51:21 +0200 Subject: [PATCH 10/12] Added example configs for recording or backtesting --- .../ta4j/backtesting/email-alerts.yaml | 24 +++++++ config/samples/ta4j/backtesting/engine.yaml | 35 ++++++++++ config/samples/ta4j/backtesting/exchange.yaml | 46 +++++++++++++ config/samples/ta4j/backtesting/markets.yaml | 35 ++++++++++ .../samples/ta4j/backtesting/strategies.yaml | 39 +++++++++++ .../samples/ta4j/recording/email-alerts.yaml | 24 +++++++ config/samples/ta4j/recording/engine.yaml | 35 ++++++++++ config/samples/ta4j/recording/exchange.yaml | 65 +++++++++++++++++++ config/samples/ta4j/recording/markets.yaml | 35 ++++++++++ config/samples/ta4j/recording/strategies.yaml | 37 +++++++++++ 10 files changed, 375 insertions(+) create mode 100644 config/samples/ta4j/backtesting/email-alerts.yaml create mode 100644 config/samples/ta4j/backtesting/engine.yaml create mode 100644 config/samples/ta4j/backtesting/exchange.yaml create mode 100644 config/samples/ta4j/backtesting/markets.yaml create mode 100644 config/samples/ta4j/backtesting/strategies.yaml create mode 100644 config/samples/ta4j/recording/email-alerts.yaml create mode 100644 config/samples/ta4j/recording/engine.yaml create mode 100644 config/samples/ta4j/recording/exchange.yaml create mode 100644 config/samples/ta4j/recording/markets.yaml create mode 100644 config/samples/ta4j/recording/strategies.yaml diff --git a/config/samples/ta4j/backtesting/email-alerts.yaml b/config/samples/ta4j/backtesting/email-alerts.yaml new file mode 100644 index 000000000..d591e38e0 --- /dev/null +++ b/config/samples/ta4j/backtesting/email-alerts.yaml @@ -0,0 +1,24 @@ +############################################################################################ +# Email Alerts YAML config. +# +# - All fields are mandatory unless stated otherwise. +# - Only 1 emailAlerts block can be specified. +# - The email is sent using TLS. +# - The indentation levels are significant in YAML: https://en.wikipedia.org/wiki/YAML +# +# Sample config for using a Gmail account to send the email is shown below. +############################################################################################ +--- +emailAlerts: + + # If set to true, the bot will load the smtpConfig, and enable email alerts. + enabled: false + + # Set your SMTP details here. + smtpConfig: + host: smtp.gmail.com + tlsPort: 587 + accountUsername: your.account.username@gmail.com + accountPassword: your.account.password + fromAddress: from.addr@gmail.com + toAddress: to.addr@gmail.com diff --git a/config/samples/ta4j/backtesting/engine.yaml b/config/samples/ta4j/backtesting/engine.yaml new file mode 100644 index 000000000..c15909df3 --- /dev/null +++ b/config/samples/ta4j/backtesting/engine.yaml @@ -0,0 +1,35 @@ +############################################################################################ +# Trading Engine YAML config. +# +# - All fields are mandatory unless stated otherwise. +# - Only 1 engine block can be specified. +# - The indentation levels are significant in YAML: https://en.wikipedia.org/wiki/YAML +############################################################################################ +--- +engine: + + # A unique identifier for the bot. Value must be an alphanumeric string. + # Underscores and dashes are also permitted. E.g. my-bitstamp-bot_1 + botId: my-ta4j-backtest-bot + + # A friendly name for the bot. Value must be an alphanumeric string. Spaces are allowed. E.g. Bitstamp Bot + botName: TA4J Backtest Bot + + # This must be set to prevent catastrophic loss on the exchange. + # This is normally the currency you intend to hold a long position in. It should be set to the currency short code + # for the wallet, e.g. BTC, LTC, USD. This value can be case sensitive for some exchanges - check the Exchange Adapter + # documentation. + emergencyStopCurrency: ZEUR + + # This must be set to prevent a catastrophic loss on the exchange. + # The Trading Engine checks this value at the start of every trade cycle: if your emergencyStopCurrency balance on + # the trading drops below this value, the Trading Engine will stop trading on all markets and shutdown. + # Manual intervention is then required to restart the bot. You can set this value to 0 to override this check. + emergencyStopBalance: 50.0 + + # The is the interval in seconds that the Trading Engine will wait/sleep before executing + # the next trade cycle. The minimum value is 1 second. Some exchanges allow you to hit them harder than others. + # However, while their API documentation might say one thing, the reality is you might get socket timeouts and 5XX + # responses if you hit it too hard - you cannot perform ultra low latency trading over the public internet ;-) + # You'll need to experiment with the trade cycle interval for different exchanges. + tradeCycleInterval: 0 diff --git a/config/samples/ta4j/backtesting/exchange.yaml b/config/samples/ta4j/backtesting/exchange.yaml new file mode 100644 index 000000000..797087642 --- /dev/null +++ b/config/samples/ta4j/backtesting/exchange.yaml @@ -0,0 +1,46 @@ +############################################################################################ +# Exchange Adapter YAML config. +# +# - Sample config below currently set to run against a totally stubbed exchange which +# is based on TA4J for backtesting a given strategy +# - All fields are mandatory unless stated otherwise. +# - BX-bot only supports running 1 exchange per bot. +# - The indentation levels are significant in YAML: https://en.wikipedia.org/wiki/YAML +# +# See the README "How do I write my own Exchange Adapter?" section for more details. +############################################################################################ +--- +exchange: + + # A friendly name for the Exchange. Value must be an alphanumeric string. Spaces are allowed. + name: ta4j + + # For the adapter value, you must specify the fully qualified name of your Exchange Adapter class so the Trading Engine + # can load and execute it. The class must be on the runtime classpath. + adapter: com.gazbert.bxbot.exchanges.TA4JRecordingAdapter + + otherConfig: + # Exchange Order fee in % for the given market. It counts for sell as well as for buy orders in the simulation + order-fee: 0.26 + + # the counter currency which is simulated. This must fit to your used markets counter currency + simulatedCounterCurrency: ZEUR + + # the starting balance for the simulation. The simulation starts with this amount as counter currency and 0 as base currency + counterCurrencyStartingBalance: 100 + + # the base currency which is simulated. This must fit to your used markets base currency + simulatedBaseCurrency: XXRP + + # the path to a json containing a recording of a market with ta4j BarSeries + # See the example ta4j strategies to find out how to record market or to simulate a strategy + #trading-series-json-path: barData_1618580976288.json + trading-series-json-path: path-to-your-json + + # if enabled, the simulation will open + # a) a graph for the executed strategy on the market, showing the buy and sell points + # b) a graph showing the optimal trading is opened besides your executed trades to compare your trades to the optimum + # No matter if you enable chart printing or not, the results of the simulation are printed to log on simulation end + generate-order-overview-charts: true + + diff --git a/config/samples/ta4j/backtesting/markets.yaml b/config/samples/ta4j/backtesting/markets.yaml new file mode 100644 index 000000000..19cb25802 --- /dev/null +++ b/config/samples/ta4j/backtesting/markets.yaml @@ -0,0 +1,35 @@ +############################################################################################ +# Market YAML config. +# +# - All fields are mandatory unless stated otherwise. +# - Multiple market blocks can be listed. +# - The indentation levels are significant in YAML: https://en.wikipedia.org/wiki/YAML +############################################################################################ +--- +markets: + + # The id value is the market id as defined on the exchange, e.g. 'btcusd'. + - id: XRPEUR + + # A friendly name for the market. Value must be an alphanumeric string. Spaces are allowed. E.g. BTC/USD + name: XRP/EUR + + # The baseCurrency value is the currency short code for the base currency in the currency pair. When you buy or + # sell a currency pair, you are performing that action on the base currency. The base currency is the commodity you + # are buying or selling. E.g. in a BTC/USD market, the first currency (BTC) is the base currency and the second + # currency (USD) is the counter currency. + baseCurrency: XXRP + + # The counterCurrency value is the currency short code for the counter currency in the currency pair. This is also + # known as the quote currency. + counterCurrency: ZEUR + + # The enabled value allows you toggle trading on the market - config changes are only applied on startup. + enabled: true + + # The tradingStrategyId value must match a strategy id defined in your strategies.yaml config. + # Currently, BX-bot only supports 1 strategy per market. + + tradingStrategyId: ta4j-backtest-strategy + + diff --git a/config/samples/ta4j/backtesting/strategies.yaml b/config/samples/ta4j/backtesting/strategies.yaml new file mode 100644 index 000000000..fc2bd68fd --- /dev/null +++ b/config/samples/ta4j/backtesting/strategies.yaml @@ -0,0 +1,39 @@ +############################################################################################ +# Trading Strategy YAML config. +# +# - You configure the loading of your strategy using either a className or a beanName field. +# - All fields are mandatory unless stated otherwise. +# - Multiple strategy blocks can be listed. +# - The indentation levels are significant in YAML: https://en.wikipedia.org/wiki/YAML +# +# See the README "How do I write my own Trading Strategy?" section for full details. +############################################################################################ +--- +strategies: + + # A unique identifier for the strategy. The markets.yaml tradingStrategyId entries reference this. + # Value must be an alphanumeric string. Underscores and dashes are also permitted. E.g. scalping-strategy + - id: ta4j-backtest-strategy + + # A friendly name for the strategy. Value must be an alphanumeric string. Spaces are allowed. E.g. My Super Strat + name: ta4j backtest strategy + + # The description value is optional. + description: > + a strategy showing how to backtest a strategy with the ta4j backtest exchange adapter. The important parts are + a) that the getTicker is called exactly once every execution cicle + b) only at max one order is placed in every trading cycle + + # For the className value, you must specify the fully qualified name of your Strategy class for the Trading Engine + # to load and execute. This class must be on the runtime classpath. + # If you set this value to load your strategy, you cannot set the beanName value. + # + #className: com.gazbert.bxbot.strategies.ExampleTA4JBacktestStrategy + + # For the beanName value, you must specify the Spring bean name of you Strategy component class + # for the Trading Engine to load and execute. + # You will also need to annotate your strategy class with `@Component("exampleScalpingStrategy")` - + # take a look at ExampleScalpingStrategy.java. This results in Spring injecting the bean. + # (see https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/stereotype/Component.html) + # If you set this value to load your strategy, you cannot set the className value. + beanName: exampleTa4jBacktestStrategy \ No newline at end of file diff --git a/config/samples/ta4j/recording/email-alerts.yaml b/config/samples/ta4j/recording/email-alerts.yaml new file mode 100644 index 000000000..d591e38e0 --- /dev/null +++ b/config/samples/ta4j/recording/email-alerts.yaml @@ -0,0 +1,24 @@ +############################################################################################ +# Email Alerts YAML config. +# +# - All fields are mandatory unless stated otherwise. +# - Only 1 emailAlerts block can be specified. +# - The email is sent using TLS. +# - The indentation levels are significant in YAML: https://en.wikipedia.org/wiki/YAML +# +# Sample config for using a Gmail account to send the email is shown below. +############################################################################################ +--- +emailAlerts: + + # If set to true, the bot will load the smtpConfig, and enable email alerts. + enabled: false + + # Set your SMTP details here. + smtpConfig: + host: smtp.gmail.com + tlsPort: 587 + accountUsername: your.account.username@gmail.com + accountPassword: your.account.password + fromAddress: from.addr@gmail.com + toAddress: to.addr@gmail.com diff --git a/config/samples/ta4j/recording/engine.yaml b/config/samples/ta4j/recording/engine.yaml new file mode 100644 index 000000000..ff88ad15b --- /dev/null +++ b/config/samples/ta4j/recording/engine.yaml @@ -0,0 +1,35 @@ +############################################################################################ +# Trading Engine YAML config. +# +# - All fields are mandatory unless stated otherwise. +# - Only 1 engine block can be specified. +# - The indentation levels are significant in YAML: https://en.wikipedia.org/wiki/YAML +############################################################################################ +--- +engine: + + # A unique identifier for the bot. Value must be an alphanumeric string. + # Underscores and dashes are also permitted. E.g. my-bitstamp-bot_1 + botId: my-ta4j-backtest-bot + + # A friendly name for the bot. Value must be an alphanumeric string. Spaces are allowed. E.g. Bitstamp Bot + botName: TA4J Backtest Bot + + # This must be set to prevent catastrophic loss on the exchange. + # This is normally the currency you intend to hold a long position in. It should be set to the currency short code + # for the wallet, e.g. BTC, LTC, USD. This value can be case sensitive for some exchanges - check the Exchange Adapter + # documentation. + emergencyStopCurrency: ZEUR + + # This must be set to prevent a catastrophic loss on the exchange. + # The Trading Engine checks this value at the start of every trade cycle: if your emergencyStopCurrency balance on + # the trading drops below this value, the Trading Engine will stop trading on all markets and shutdown. + # Manual intervention is then required to restart the bot. You can set this value to 0 to override this check. + emergencyStopBalance: 50.0 + + # The is the interval in seconds that the Trading Engine will wait/sleep before executing + # the next trade cycle. The minimum value is 1 second. Some exchanges allow you to hit them harder than others. + # However, while their API documentation might say one thing, the reality is you might get socket timeouts and 5XX + # responses if you hit it too hard - you cannot perform ultra low latency trading over the public internet ;-) + # You'll need to experiment with the trade cycle interval for different exchanges. + tradeCycleInterval: 3 diff --git a/config/samples/ta4j/recording/exchange.yaml b/config/samples/ta4j/recording/exchange.yaml new file mode 100644 index 000000000..2826fb18a --- /dev/null +++ b/config/samples/ta4j/recording/exchange.yaml @@ -0,0 +1,65 @@ +############################################################################################ +# Exchange Adapter YAML config. +# +# - Sample config below currently set to run against Kraken. +# - All fields are mandatory unless stated otherwise. +# - BX-bot only supports running 1 exchange per bot. +# - The indentation levels are significant in YAML: https://en.wikipedia.org/wiki/YAML +# +# See the README "How do I write my own Exchange Adapter?" section for more details. +############################################################################################ +--- +exchange: + + # A friendly name for the Exchange. Value must be an alphanumeric string. Spaces are allowed. + name: Kraken + + # For the adapter value, you must specify the fully qualified name of your Exchange Adapter class so the Trading Engine + # can load and execute it. The class must be on the runtime classpath. + adapter: com.gazbert.bxbot.exchanges.KrakenExchangeAdapter + + authenticationConfig: + # See https://www.kraken.com/u/settings/api to get your Kraken Trading API credentials. + key: your-api-key + secret: your-secret-key + + networkConfig: + # This value is in SECONDS. It is the timeout value that the exchange adapter will wait on socket connect/socket read + # when communicating with the exchange. Once this threshold has been breached, the exchange adapter will give up and + # throw a Trading API TimeoutException. + # + # The exchange adapter is single threaded: if one request gets blocked, it will block all subsequent requests from + # getting to the exchange. This timeout prevents an indefinite block. + # + # You'll need to experiment with values here. + connectionTimeout: 30 + + # Optional HTTP status codes that will trigger the adapter to throw a non-fatal ExchangeNetworkException + # if the exchange returns any of the below in an API call response: + nonFatalErrorCodes: [502, 503, 504, 520, 522, 525] + + # Optional java.io exception messages that will trigger the adapter to throw a non-fatal ExchangeNetworkException + # if the exchange returns any of the below in an API call response: + nonFatalErrorMessages: + - Connection reset + - Connection refused + - Remote host closed connection during handshake + - Unexpected end of file from server + + otherConfig: + # Exchange Taker Buy fee in % for XBTGBP market + # IMPORTANT - keep an eye on the fees: https://www.kraken.com/help/fees + # Taker fee on 29 Jul 2016 = 0.26% + buy-fee: 0.26 + + # Exchange Taker Sell fee in % for XBTGBP market + # IMPORTANT - keep an eye on the fees: https://www.kraken.com/help/fees + # Taker fee on 29 Jul 2016 = 0.26% + sell-fee: 0.26 + + # If set to true, the bot will stay up when the exchange is undergoing maintenance - the adapter will throw a + # ExchangeNetworkException. + # + # If set to false, the bot will shut down if the exchange is undergoing maintenance - the adapter will throw a + # fatal TradingApiException. + keep-alive-during-maintenance: false diff --git a/config/samples/ta4j/recording/markets.yaml b/config/samples/ta4j/recording/markets.yaml new file mode 100644 index 000000000..f869de975 --- /dev/null +++ b/config/samples/ta4j/recording/markets.yaml @@ -0,0 +1,35 @@ +############################################################################################ +# Market YAML config. +# +# - All fields are mandatory unless stated otherwise. +# - Multiple market blocks can be listed. +# - The indentation levels are significant in YAML: https://en.wikipedia.org/wiki/YAML +############################################################################################ +--- +markets: + + # The id value is the market id as defined on the exchange, e.g. 'btcusd'. + - id: XRPEUR + + # A friendly name for the market. Value must be an alphanumeric string. Spaces are allowed. E.g. BTC/USD + name: XRP/EUR + + # The baseCurrency value is the currency short code for the base currency in the currency pair. When you buy or + # sell a currency pair, you are performing that action on the base currency. The base currency is the commodity you + # are buying or selling. E.g. in a BTC/USD market, the first currency (BTC) is the base currency and the second + # currency (USD) is the counter currency. + baseCurrency: XXRP + + # The counterCurrency value is the currency short code for the counter currency in the currency pair. This is also + # known as the quote currency. + counterCurrency: ZEUR + + # The enabled value allows you toggle trading on the market - config changes are only applied on startup. + enabled: true + + # The tradingStrategyId value must match a strategy id defined in your strategies.yaml config. + # Currently, BX-bot only supports 1 strategy per market. + + tradingStrategyId: ta4j-recording-strategy + + diff --git a/config/samples/ta4j/recording/strategies.yaml b/config/samples/ta4j/recording/strategies.yaml new file mode 100644 index 000000000..f95bdc534 --- /dev/null +++ b/config/samples/ta4j/recording/strategies.yaml @@ -0,0 +1,37 @@ +############################################################################################ +# Trading Strategy YAML config. +# +# - You configure the loading of your strategy using either a className or a beanName field. +# - All fields are mandatory unless stated otherwise. +# - Multiple strategy blocks can be listed. +# - The indentation levels are significant in YAML: https://en.wikipedia.org/wiki/YAML +# +# See the README "How do I write my own Trading Strategy?" section for full details. +############################################################################################ +--- +strategies: + + # A unique identifier for the strategy. The markets.yaml tradingStrategyId entries reference this. + # Value must be an alphanumeric string. Underscores and dashes are also permitted. E.g. scalping-strategy + - id: ta4j-recording-strategy + + # A friendly name for the strategy. Value must be an alphanumeric string. Spaces are allowed. E.g. My Super Strat + name: ta4j recording strategy + + # The description value is optional. + description: > + a strategy showing how to live record a market from a real exchange into a ta4j barseries and stores it as json + + # For the className value, you must specify the fully qualified name of your Strategy class for the Trading Engine + # to load and execute. This class must be on the runtime classpath. + # If you set this value to load your strategy, you cannot set the beanName value. + # + #className: com.gazbert.bxbot.strategies.ExampleTA4JRecordingStrategy + + # For the beanName value, you must specify the Spring bean name of you Strategy component class + # for the Trading Engine to load and execute. + # You will also need to annotate your strategy class with `@Component("exampleScalpingStrategy")` - + # take a look at ExampleScalpingStrategy.java. This results in Spring injecting the bean. + # (see https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/stereotype/Component.html) + # If you set this value to load your strategy, you cannot set the className value. + beanName: exampleTa4jRecordingStrategy \ No newline at end of file From 4d8c387ac8eddb166786a1b64de65895b681f17f Mon Sep 17 00:00:00 2001 From: Marc Dahlem Date: Fri, 23 Apr 2021 01:15:43 +0200 Subject: [PATCH 11/12] Speedup gradle build --- build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/build.gradle b/build.gradle index 3d5af7852..49f11d921 100644 --- a/build.gradle +++ b/build.gradle @@ -107,6 +107,10 @@ allprojects { group = 'com.gazbert.bxbot' version = '1.2.1-SNAPSHOT' + + dependencyManagement { + applyMavenExclusions = false + } } subprojects { From 1c8c2c069a9de9ceef9da7820dae37aa6a67f0d8 Mon Sep 17 00:00:00 2001 From: Marc Dahlem Date: Sat, 24 Apr 2021 20:48:31 +0200 Subject: [PATCH 12/12] Corrected timestamps --- .../java/com/gazbert/bxbot/exchanges/TA4JRecordingAdapter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/TA4JRecordingAdapter.java b/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/TA4JRecordingAdapter.java index 128b6029f..dcbcbcc12 100644 --- a/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/TA4JRecordingAdapter.java +++ b/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/TA4JRecordingAdapter.java @@ -165,7 +165,7 @@ public Ticker getTicker(String marketId) throws TradingApiException, ExchangeNet BigDecimal open = (BigDecimal) currentBar.getOpenPrice().getDelegate(); BigDecimal volume = (BigDecimal) currentBar.getVolume().getDelegate(); BigDecimal vwap = BigDecimal.ZERO; - Long timestamp = currentBar.getEndTime().toEpochSecond(); + Long timestamp = currentBar.getEndTime().toInstant().toEpochMilli(); return new TickerImpl(last, bid, ask, low, high, open, volume, vwap, timestamp); }