diff --git a/build.gradle b/build.gradle
index e420c4dd8..49f11d921 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),
@@ -105,6 +107,10 @@ allprojects {
group = 'com.gazbert.bxbot'
version = '1.2.1-SNAPSHOT'
+
+ dependencyManagement {
+ applyMavenExclusions = false
+ }
}
subprojects {
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..dcbcbcc12
--- /dev/null
+++ b/bxbot-exchanges/src/main/java/com/gazbert/bxbot/exchanges/TA4JRecordingAdapter.java
@@ -0,0 +1,273 @@
+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 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";
+ 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 orderFeePercentage;
+ 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;
+ 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 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);
+
+ 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);
+
+ final String shouldGenerateChartsInConfig = getOtherConfigItem(otherConfig, SHOULD_GENERATE_CHARTS_PROPERTY_NAME);
+ shouldPrintCharts = Boolean.parseBoolean(shouldGenerateChartsInConfig);
+ LOG.info(() -> "Should print charts at simulation end: " + shouldPrintCharts);
+
+ }
+
+ @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 (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(marketId);
+ return newOrderID;
+ }
+
+ @Override
+ public boolean cancelOrder(String orderId, String marketId) throws ExchangeNetworkException, TradingApiException {
+ 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(simulatedBaseCurrency, baseCurrencyBalance);
+ availableBalances.put(simulatedCounterCurrency, 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(marketId);
+ return null;
+ }
+
+ checkOpenOrderExecution(marketId);
+
+ Bar currentBar = tradingSeries.getBar(currentTick);
+ BigDecimal last = (BigDecimal) currentBar.getClosePrice().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();
+ BigDecimal volume = (BigDecimal) currentBar.getVolume().getDelegate();
+ BigDecimal vwap = BigDecimal.ZERO;
+ Long timestamp = currentBar.getEndTime().toInstant().toEpochMilli();
+ return new TickerImpl(last, bid, ask, low, high, open, volume, vwap, timestamp);
+ }
+
+ private void checkOpenOrderExecution(String marketId) throws TradingApiException, ExchangeNetworkException {
+ if (currentOpenOrder != null) {
+ switch (currentOpenOrder.getType()) {
+ case BUY:
+ checkOpenBuyOrderExecution(marketId);
+ break;
+ case SELL:
+ checkOpenSellOrderExecution(marketId);
+ break;
+ default:
+ 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
+ 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);
+ BigDecimal netOrderPrice = orderPrice.subtract(buyFees);
+ counterCurrencyBalance = counterCurrencyBalance.add(netOrderPrice);
+ baseCurrencyBalance = baseCurrencyBalance.subtract(currentOpenOrder.getOriginalQuantity());
+ currentOpenOrder = null;
+ }
+ }
+
+ 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) {
+ 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);
+ BigDecimal netOrderPrice = orderPrice.add(buyFees);
+ counterCurrencyBalance = counterCurrencyBalance.subtract(netOrderPrice);
+ baseCurrencyBalance = baseCurrencyBalance.add(currentOpenOrder.getOriginalQuantity());
+ currentOpenOrder = null;
+ }
+ }
+
+
+ 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));
+ 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);
+ if (shouldPrintCharts) {
+ 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 orderFeePercentage;
+ }
+
+ @Override
+ public BigDecimal getPercentageOfSellOrderTakenForExchangeFee(String marketId) throws TradingApiException, ExchangeNetworkException {
+ return orderFeePercentage;
+ }
+}
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/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
+ 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;
+ }
+}
diff --git a/bxbot-strategies/src/main/java/com/gazbert/bxbot/strategies/ExampleTA4JRecordingStrategy.java b/bxbot-strategies/src/main/java/com/gazbert/bxbot/strategies/ExampleTA4JRecordingStrategy.java
new file mode 100644
index 000000000..820ac3d52
--- /dev/null
+++ b/bxbot-strategies/src/main/java/com/gazbert/bxbot/strategies/ExampleTA4JRecordingStrategy.java
@@ -0,0 +1,63 @@
+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 java.math.BigDecimal;
+import java.time.ZonedDateTime;
+
+@Component("exampleTa4jRecordingStrategy") // used to load the strategy using Spring bean injection
+public class ExampleTA4JRecordingStrategy implements TradingStrategy {
+
+ private static final Logger LOG = LogManager.getLogger();
+ private TradingApi tradingApi;
+ private Market market;
+ private BarSeries series;
+
+ @Override
+ public void init(TradingApi tradingApi, Market market, StrategyConfig config) {
+ LOG.info(() -> "Initialising TA4J Recording Strategy...");
+ this.tradingApi = tradingApi;
+ this.market = market;
+ series = new BaseBarSeriesBuilder().withName(market.getName() + "_" + System.currentTimeMillis()).build();
+ LOG.info(() -> "Trading Strategy initialised successfully!");
+ }
+
+ @Override
+ public void execute() throws StrategyException {
+ try {
+
+ Ticker 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
+ 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;
+ }
+}
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
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
+