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 +