/
ExampleScalpingStrategy.java
711 lines (638 loc) · 30.5 KB
/
ExampleScalpingStrategy.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
/*
* The MIT License (MIT)
*
* Copyright (c) 2015 Gareth Jon Lynch
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.gazbert.bxbot.strategies;
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.ExchangeNetworkException;
import com.gazbert.bxbot.trading.api.Market;
import com.gazbert.bxbot.trading.api.MarketOrder;
import com.gazbert.bxbot.trading.api.MarketOrderBook;
import com.gazbert.bxbot.trading.api.OpenOrder;
import com.gazbert.bxbot.trading.api.OrderType;
import com.gazbert.bxbot.trading.api.TradingApi;
import com.gazbert.bxbot.trading.api.TradingApiException;
import com.google.common.base.MoreObjects;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.DecimalFormat;
import java.util.List;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Component;
/**
* This is a very simple <a
* href="http://www.investopedia.com/articles/trading/02/081902.asp">scalping strategy</a> to show
* how to use the Trading API; you will want to code a much better algorithm! It trades using <a
* href="http://www.investopedia.com/terms/l/limitorder.asp">limit orders</a> at the <a
* href="http://www.investopedia.com/terms/s/spotprice.asp">spot price</a>.
*
* <p><strong> DISCLAIMER: This algorithm is provided as-is; it might have bugs in it and you could
* lose money. Use it at our own risk! </strong>
*
* <p>It was originally written to trade on <a href="https://btc-e.com">BTC-e</a>, but should work
* for any exchange. The algorithm will start by buying the base currency (BTC in this example)
* using the counter currency (USD in this example), and then sell the base currency (BTC) at a
* higher price to take profit from the spread. The algorithm expects you to have deposited
* sufficient counter currency (USD) into your exchange wallet in order to buy the base currency
* (BTC).
*
* <p>When it starts up, it places an order at the current BID price and uses x amount of counter
* currency (USD) to 'buy' the base currency (BTC). The value of x comes from the sample
* {project-root}/config/strategies.yaml 'counter-currency-buy-order-amount' config-item, currently
* set to 20 USD. Make sure that the value you use for x is large enough to be able to meet the
* minimum BTC order size for the exchange you are trading on, e.g. the Bitfinex min order size is
* 0.01 BTC as of 3 May 2017. The algorithm then waits for the buy order to fill...
*
* <p>Once the buy order fills, it then waits until the ASK price is at least y % higher than the
* previous buy fill price. The value of y comes from the sample
* {project-root}/config/strategies.yaml 'minimum-percentage-gain' config-item, currently set to 1%.
* Once the % gain has been achieved, the algorithm will place a sell order at the current ASK
* price. It then waits for the sell order to fill... and the cycle repeats.
*
* <p>The algorithm does not factor in being outbid when placing buy orders, i.e. it does not cancel
* the current order and place a new order at a higher price; it simply holds until the current BID
* price falls again. Likewise, the algorithm does not factor in being undercut when placing sell
* orders; it does not cancel the current order and place a new order at a lower price.
*
* <p>Chances are you will either get a stuck buy order if the market is going up, or a stuck sell
* order if the market goes down. You could manually execute the trades on the exchange and restart
* the bot to get going again... but a much better solution would be to modify this code to deal
* with it: cancel your current buy order and place a new order matching the current BID price, or
* cancel your current sell order and place a new order matching the current ASK price. The {@link
* TradingApi} allows you to add this behaviour.
*
* <p>Remember to include the correct exchange fees (both buy and sell) in your buy/sell
* calculations when you write your own algorithm. Otherwise, you'll end up bleeding fiat/crypto to
* the exchange...
*
* <p>This demo algorithm relies on the {project-root}/config/strategies.yaml
* 'minimum-percentage-gain' config-item value being high enough to make a profit and cover the
* exchange fees. You could tweak the algo to call the {@link
* TradingApi#getPercentageOfBuyOrderTakenForExchangeFee(String)} and {@link
* TradingApi#getPercentageOfSellOrderTakenForExchangeFee(String)} when calculating the order to
* send to the exchange... See the sample {project-root}/config/samples/{exchange}/exchange.yaml
* files for info on the different exchange fees.
*
* <p>You configure the loading of your strategy using either a className OR a beanName in the
* {project-root}/config/strategies.yaml config file. This example strategy is configured using the
* bean-name and by setting the @Component("exampleScalpingStrategy") annotation - this results in
* Spring injecting the bean - see <a
* href="https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/stereotype/Component.html">
* Spring docs</a> for more details. Alternatively, you can load your strategy using className -
* this will use the bot's custom injection framework. The choice is yours, but beanName is the way
* to go if you want to use other Spring features in your strategy, e.g. a <a
* href="https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/stereotype/Repository.html">
* Repository</a> to store your trade data.
*
* <p>The algorithm relies on config from the sample {project-root}/config/strategies.yaml and
* {project-root}/config/markets.yaml files. You can pass additional configItems to your Strategy
* using the {project-root}/config/strategies.yaml file - you access it from the {@link
* #init(TradingApi, Market, StrategyConfig)} method via the StrategyConfigImpl argument.
*
* <p>This simple demo algorithm only manages 1 order at a time to keep things simple.
*
* <p>The Trading Engine will only send 1 thread through your strategy code at a time - you do not
* have to code for concurrency.
*
* <p>This <a
* href="http://www.investopedia.com/articles/active-trading/101014/basics-algorithmic-trading-concepts-and-examples.asp">
* site</a> might give you a few ideas - the {@link TradingApi} provides a basic Ticker that you
* might want to use. Check out the excellent <a href="https://github.com/ta4j/ta4j">ta4j</a>
* project too.
*
* <p>Good luck!
*
* @author gazbert
*/
@Component("exampleScalpingStrategy") // used to load the strategy using Spring bean injection
@Log4j2
public class ExampleScalpingStrategy implements TradingStrategy {
/** The decimal format for the logs. */
private static final String DECIMAL_FORMAT = "#.########";
/** Reference to the main Trading API. */
private TradingApi tradingApi;
/** The market this strategy is trading on. */
private Market market;
/** The state of the order. */
private OrderState lastOrder;
/**
* The counter currency amount to use when placing the buy order. This was loaded from the
* strategy entry in the {project-root}/config/strategies.yaml config file.
*/
private BigDecimal counterCurrencyBuyOrderAmount;
/**
* The minimum % gain was to achieve before placing a SELL oder. This was loaded from the strategy
* entry in the {project-root}/config/strategies.yaml config file.
*/
private BigDecimal minimumPercentageGain;
/** Constructs the Example Scalping Strategy. */
public ExampleScalpingStrategy() {
// No extra init.
}
/**
* Initialises the Trading Strategy. Called once by the Trading Engine when the bot starts up;
* it's a bit like a servlet init() method.
*
* @param tradingApi the Trading API. Use this to make trades and stuff.
* @param market the market for this strategy. This is the market the strategy is currently
* running on - you wire this up in the markets.yaml and strategies.yaml files.
* @param config configuration for the strategy. Contains any (optional) config you set up in the
* strategies.yaml file.
*/
@Override
public void init(TradingApi tradingApi, Market market, StrategyConfig config) {
log.info("Initialising Trading Strategy...");
this.tradingApi = tradingApi;
this.market = market;
getConfigForStrategy(config);
log.info("Trading Strategy initialised successfully!");
}
/**
* This is the main execution method of the Trading Strategy. It is where your algorithm lives.
*
* <p>It is called by the Trading Engine during each trade cycle, e.g. every 60s. The trade cycle
* is configured in the {project-root}/config/engine.yaml file.
*
* @throws StrategyException if something unexpected occurs. This tells the Trading Engine to shut
* down the bot immediately to help prevent unexpected losses.
*/
@Override
public void execute() throws StrategyException {
log.info(market.getName() + " Checking order status...");
try {
// Grab the latest order book for the market.
final MarketOrderBook orderBook = tradingApi.getMarketOrders(market.getId());
final List<MarketOrder> buyOrders = orderBook.getBuyOrders();
if (buyOrders.isEmpty()) {
log.warn(
"Exchange returned empty Buy Orders. Ignoring this trade window. OrderBook: "
+ orderBook);
return;
}
final List<MarketOrder> sellOrders = orderBook.getSellOrders();
if (sellOrders.isEmpty()) {
log.warn(
"Exchange returned empty Sell Orders. Ignoring this trade window. OrderBook: "
+ orderBook);
return;
}
// Get the current BID and ASK spot prices.
final BigDecimal currentBidPrice = buyOrders.get(0).getPrice();
final BigDecimal currentAskPrice = sellOrders.get(0).getPrice();
log.info(
market.getName()
+ " Current BID price="
+ new DecimalFormat(DECIMAL_FORMAT).format(currentBidPrice));
log.info(
market.getName()
+ " Current ASK price="
+ new DecimalFormat(DECIMAL_FORMAT).format(currentAskPrice));
// Is this the first time the Strategy has been called? If yes, we initialise the OrderState,
// so we can keep track of orders during later trace cycles.
if (lastOrder == null) {
log.info(
market.getName()
+ " First time Strategy has been called - creating new OrderState object.");
lastOrder = new OrderState();
}
// Always handy to log what the last order was during each trace cycle.
log.info(market.getName() + " Last Order was: " + lastOrder);
// Execute the appropriate algorithm based on the last order type.
if (lastOrder.type == OrderType.BUY) {
executeAlgoForWhenLastOrderWasBuy();
} else if (lastOrder.type == OrderType.SELL) {
executeAlgoForWhenLastOrderWasSell(currentBidPrice, currentAskPrice);
} else if (lastOrder.type == null) {
executeAlgoForWhenLastOrderWasNone(currentBidPrice);
}
} catch (ExchangeNetworkException e) {
// Your timeout handling code could go here.
// We are just going to log it and swallow it, and wait for next trade cycle.
log.error(
market.getName()
+ " Failed to get market orders because Exchange threw network exception. "
+ "Waiting until next trade cycle.",
e);
} catch (TradingApiException e) {
// Your error handling code could go here...
// We are just going to re-throw as StrategyException for engine to deal with - it will
// shut down the bot.
log.error(
market.getName()
+ " Failed to get market orders because Exchange threw TradingApi exception. "
+ "Telling Trading Engine to shutdown bot!",
e);
throw new StrategyException(e);
}
}
/**
* Algo for executing when the Trading Strategy is invoked for the first time. We start off with a
* buy order at current BID price.
*
* @param currentBidPrice the current market BID price.
* @throws StrategyException if an unexpected exception is received from the Exchange Adapter.
* Throwing this exception indicates we want the Trading Engine to shut down the bot.
*/
private void executeAlgoForWhenLastOrderWasNone(BigDecimal currentBidPrice)
throws StrategyException {
log.info(
market.getName()
+ " OrderType is NONE - placing new BUY order at ["
+ new DecimalFormat(DECIMAL_FORMAT).format(currentBidPrice)
+ "]");
try {
// Calculate the amount of base currency (BTC) to buy for given amount of counter currency
// (USD).
final BigDecimal amountOfBaseCurrencyToBuy =
getAmountOfBaseCurrencyToBuyForGivenCounterCurrencyAmount(counterCurrencyBuyOrderAmount);
// Send the order to the exchange
log.info(market.getName() + " Sending initial BUY order to exchange --->");
lastOrder.id =
tradingApi.createOrder(
market.getId(), OrderType.BUY, amountOfBaseCurrencyToBuy, currentBidPrice);
log.info(market.getName() + " Initial BUY Order sent successfully. ID: " + lastOrder.id);
// update last order details
lastOrder.price = currentBidPrice;
lastOrder.type = OrderType.BUY;
lastOrder.amount = amountOfBaseCurrencyToBuy;
} catch (ExchangeNetworkException e) {
// Your timeout handling code could go here, e.g. you might want to check if the order
// actually made it to the exchange? And if not, resend it...
// We are just going to log it and swallow it, and wait for next trade cycle.
log.error(
market.getName()
+ " Initial order to BUY base currency failed because Exchange threw network "
+ "exception. Waiting until next trade cycle.",
e);
} catch (TradingApiException e) {
// Your error handling code could go here...
// We are just going to re-throw as StrategyException for engine to deal with - it will
// shut down the bot.
log.error(
market.getName()
+ " Initial order to BUY base currency failed because Exchange threw TradingApi "
+ "exception. Telling Trading Engine to shutdown bot!",
e);
throw new StrategyException(e);
}
}
/**
* Algo for executing when last order we placed on the exchanges was a BUY.
*
* <p>If last buy order filled, we try and sell at a profit.
*
* @throws StrategyException if an unexpected exception is received from the Exchange Adapter.
* Throwing this exception indicates we want the Trading Engine to shut down the bot.
*/
private void executeAlgoForWhenLastOrderWasBuy() throws StrategyException {
try {
// Fetch our current open orders and see if the buy order is still outstanding/open on the
// exchange.
final List<OpenOrder> myOrders = tradingApi.getYourOpenOrders(market.getId());
boolean lastOrderFound = false;
for (final OpenOrder myOrder : myOrders) {
if (myOrder.getId().equals(lastOrder.id)) {
lastOrderFound = true;
break;
}
}
// If the order is not there, it must have all filled.
if (!lastOrderFound) {
log.info(
market.getName()
+ " ^^^ Yay!!! Last BUY Order Id ["
+ lastOrder.id
+ "] filled at ["
+ lastOrder.price
+ "]");
/*
* The last buy order was filled, so lets see if we can send a new sell order.
*
* IMPORTANT - new sell order ASK price must be > (last order price + exchange fees)
* because:
*
* 1. If we put sell amount in as same amount as previous buy, the exchange barfs because
* we don't have enough units to cover the transaction fee.
* 2. We could end up selling at a loss.
*
* For this example strategy, we're just going to add 2% (taken from the
* 'minimum-percentage-gain' config item in the {project-root}/config/strategies.yaml
* config file) on top of previous bid price to make a little profit and cover the exchange
* fees.
*
* Your algo will have other ideas on how much profit to make and when to apply the
* exchange fees - you could try calling the
* TradingApi#getPercentageOfBuyOrderTakenForExchangeFee() and
* TradingApi#getPercentageOfSellOrderTakenForExchangeFee() when calculating the order to
* send to the exchange...
*/
log.info(
market.getName()
+ " Percentage profit (in decimal) to make for the sell order is: "
+ minimumPercentageGain);
final BigDecimal amountToAdd = lastOrder.price.multiply(minimumPercentageGain);
log.info(market.getName() + " Amount to add to last buy order fill price: " + amountToAdd);
// Most exchanges (if not all) use 8 decimal places.
// It's usually best to round up the ASK price in your calculations to maximise gains.
final BigDecimal newAskPrice =
lastOrder.price.add(amountToAdd).setScale(8, RoundingMode.HALF_UP);
log.info(
market.getName()
+ " Placing new SELL order at ask price ["
+ new DecimalFormat(DECIMAL_FORMAT).format(newAskPrice)
+ "]");
log.info(market.getName() + " Sending new SELL order to exchange --->");
// Build the new sell order
lastOrder.id =
tradingApi.createOrder(market.getId(), OrderType.SELL, lastOrder.amount, newAskPrice);
log.info(market.getName() + " New SELL Order sent successfully. ID: " + lastOrder.id);
// update last order state
lastOrder.price = newAskPrice;
lastOrder.type = OrderType.SELL;
} else {
/*
* BUY order has not filled yet.
* Could be nobody has jumped on it yet... or the order is only part filled... or market
* has gone up, and we've been outbid and have a stuck buy order. In which case, we have to
* wait for the market to fall for the order to fill... or you could tweak this code to
* cancel the current order and raise your bid - remember to deal with any part-filled
* orders!
*/
log.info(
market.getName()
+ " !!! Still have BUY Order "
+ lastOrder.id
+ " waiting to fill at ["
+ lastOrder.price
+ "] - holding last BUY order...");
}
} catch (ExchangeNetworkException e) {
// Your timeout handling code could go here, e.g. you might want to check if the order
// actually
// made it to the exchange? And if not, resend it...
// We are just going to log it and swallow it, and wait for next trade cycle.
log.error(
market.getName()
+ " New Order to SELL base currency failed because Exchange threw network "
+ "exception. Waiting until next trade cycle. Last Order: "
+ lastOrder,
e);
} catch (TradingApiException e) {
// Your error handling code could go here...
// We are just going to re-throw as StrategyException for engine to deal with - it will
// shut down the bot.
log.error(
market.getName()
+ " New order to SELL base currency failed because Exchange threw TradingApi "
+ "exception. Telling Trading Engine to shutdown bot! Last Order: "
+ lastOrder,
e);
throw new StrategyException(e);
}
}
/**
* Algo for executing when last order we placed on the exchange was a SELL.
*
* <p>If last sell order filled, we send a new buy order to the exchange.
*
* @param currentBidPrice the current market BID price.
* @param currentAskPrice the current market ASK price.
* @throws StrategyException if an unexpected exception is received from the Exchange Adapter.
* Throwing this exception indicates we want the Trading Engine to shut down the bot.
*/
private void executeAlgoForWhenLastOrderWasSell(
BigDecimal currentBidPrice, BigDecimal currentAskPrice) throws StrategyException {
try {
final List<OpenOrder> myOrders = tradingApi.getYourOpenOrders(market.getId());
boolean lastOrderFound = false;
for (final OpenOrder myOrder : myOrders) {
if (myOrder.getId().equals(lastOrder.id)) {
lastOrderFound = true;
break;
}
}
// If the order is not there, it must have all filled.
if (!lastOrderFound) {
log.info(
market.getName()
+ " ^^^ Yay!!! Last SELL Order Id ["
+ lastOrder.id
+ "] filled at ["
+ lastOrder.price
+ "]");
// Get amount of base currency (BTC) we can buy for given counter currency (USD) amount.
final BigDecimal amountOfBaseCurrencyToBuy =
getAmountOfBaseCurrencyToBuyForGivenCounterCurrencyAmount(
counterCurrencyBuyOrderAmount);
log.info(
market.getName()
+ " Placing new BUY order at bid price ["
+ new DecimalFormat(DECIMAL_FORMAT).format(currentBidPrice)
+ "]");
log.info(market.getName() + " Sending new BUY order to exchange --->");
// Send the buy order to the exchange.
lastOrder.id =
tradingApi.createOrder(
market.getId(), OrderType.BUY, amountOfBaseCurrencyToBuy, currentBidPrice);
log.info(market.getName() + " New BUY Order sent successfully. ID: " + lastOrder.id);
// update last order details
lastOrder.price = currentBidPrice;
lastOrder.type = OrderType.BUY;
lastOrder.amount = amountOfBaseCurrencyToBuy;
} else {
/*
* SELL order not filled yet.
* Could be nobody has jumped on it yet... or the order is only part filled... or market
* has gone down, and we've been undercut and have a stuck sell order. In which case, we
* have to wait for market to recover for the order to fill... or you could tweak this
* code to cancel the current order and lower your ask - remember to deal with any
* part-filled orders!
*/
if (currentAskPrice.compareTo(lastOrder.price) < 0) {
log.info(
market.getName()
+ " <<< Current ask price ["
+ currentAskPrice
+ "] is LOWER then last order price ["
+ lastOrder.price
+ "] - holding last SELL order...");
} else if (currentAskPrice.compareTo(lastOrder.price) > 0) {
log.error(
market.getName()
+ " >>> Current ask price ["
+ currentAskPrice
+ "] is HIGHER than last order price ["
+ lastOrder.price
+ "] - IMPOSSIBLE! BX-bot must have sold?????");
} else if (currentAskPrice.compareTo(lastOrder.price) == 0) {
log.info(
market.getName()
+ " === Current ask price ["
+ currentAskPrice
+ "] is EQUAL to last order price ["
+ lastOrder.price
+ "] - holding last SELL order...");
}
}
} catch (ExchangeNetworkException e) {
// Your timeout handling code could go here, e.g. you might want to check if the order
// actually made it to the exchange? And if not, resend it...
// We are just going to log it and swallow it, and wait for next trade cycle.
log.error(
market.getName()
+ " New Order to BUY base currency failed because Exchange threw network "
+ "exception. Waiting until next trade cycle. Last Order: "
+ lastOrder,
e);
} catch (TradingApiException e) {
// Your error handling code could go here...
// We are just going to re-throw as StrategyException for engine to deal with - it will
// shut down the bot.
log.error(
market.getName()
+ " New order to BUY base currency failed because Exchange threw TradingApi "
+ "exception. Telling Trading Engine to shutdown bot! Last Order: "
+ lastOrder,
e);
throw new StrategyException(e);
}
}
/**
* Returns amount of base currency (BTC) to buy for a given amount of counter currency (USD) based
* on last market trade price.
*
* @param amountOfCounterCurrencyToTrade the amount of counter currency (USD) we have to trade
* (buy) with.
* @return the amount of base currency (BTC) we can buy for the given counter currency (USD)
* amount.
* @throws TradingApiException if an unexpected error occurred contacting the exchange.
* @throws ExchangeNetworkException if a request to the exchange has timed out.
*/
private BigDecimal getAmountOfBaseCurrencyToBuyForGivenCounterCurrencyAmount(
BigDecimal amountOfCounterCurrencyToTrade)
throws TradingApiException, ExchangeNetworkException {
log.info(
market.getName()
+ " Calculating amount of base currency (BTC) to buy for amount of counter "
+ "currency "
+ new DecimalFormat(DECIMAL_FORMAT).format(amountOfCounterCurrencyToTrade)
+ " "
+ market.getCounterCurrency());
// Fetch the last trade price
final BigDecimal lastTradePriceInUsdForOneBtc = tradingApi.getLatestMarketPrice(market.getId());
log.info(
market.getName()
+ " Last trade price for 1 "
+ market.getBaseCurrency()
+ " was: "
+ new DecimalFormat(DECIMAL_FORMAT).format(lastTradePriceInUsdForOneBtc)
+ " "
+ market.getCounterCurrency());
/*
* Most exchanges (if not all) use 8 decimal places and typically round in favour of the
* exchange. It's usually safest to round down the order quantity in your calculations.
*/
final BigDecimal amountOfBaseCurrencyToBuy =
amountOfCounterCurrencyToTrade.divide(
lastTradePriceInUsdForOneBtc, 8, RoundingMode.HALF_DOWN);
log.info(
market.getName()
+ " Amount of base currency ("
+ market.getBaseCurrency()
+ ") to BUY for "
+ new DecimalFormat(DECIMAL_FORMAT).format(amountOfCounterCurrencyToTrade)
+ " "
+ market.getCounterCurrency()
+ " based on last market trade price: "
+ amountOfBaseCurrencyToBuy);
return amountOfBaseCurrencyToBuy;
}
/**
* Loads the config for the strategy. We expect the 'counter-currency-buy-order-amount' and
* 'minimum-percentage-gain' config items to be present in the
* {project-root}/config/strategies.yaml config file.
*
* @param config the config for the Trading Strategy.
*/
private void getConfigForStrategy(StrategyConfig config) {
// Get counter currency buy amount...
final String counterCurrencyBuyOrderAmountFromConfigAsString =
config.getConfigItem("counter-currency-buy-order-amount");
if (counterCurrencyBuyOrderAmountFromConfigAsString == null) {
// game over
throw new IllegalArgumentException(
"Mandatory counter-currency-buy-order-amount value missing in strategy.xml config.");
}
log.info(
"<counter-currency-buy-order-amount> from config is: "
+ counterCurrencyBuyOrderAmountFromConfigAsString);
// Will fail fast if value is not a number
counterCurrencyBuyOrderAmount = new BigDecimal(counterCurrencyBuyOrderAmountFromConfigAsString);
log.info("counterCurrencyBuyOrderAmount: " + counterCurrencyBuyOrderAmount);
// Get min % gain...
final String minimumPercentageGainFromConfigAsString =
config.getConfigItem("minimum-percentage-gain");
if (minimumPercentageGainFromConfigAsString == null) {
// game over
throw new IllegalArgumentException(
"Mandatory minimum-percentage-gain value missing in strategy.xml config.");
}
log.info(
"<minimum-percentage-gain> from config is: " + minimumPercentageGainFromConfigAsString);
// Will fail fast if value is not a number
final BigDecimal minimumPercentageGainFromConfig =
new BigDecimal(minimumPercentageGainFromConfigAsString);
minimumPercentageGain =
minimumPercentageGainFromConfig.divide(new BigDecimal(100), 8, RoundingMode.HALF_UP);
log.info("minimumPercentageGain in decimal is: " + minimumPercentageGain);
}
/**
* Models the state of an Order placed on the exchange.
*
* <p>Typically, you would maintain order state in a database or use some other persistence method
* to recover from restarts and for audit purposes. In this example, we are storing the state in
* memory to keep it simple.
*/
private static class OrderState {
/** Id - default to null. */
private String id = null;
/**
* Type: buy/sell. We default to null which means no order has been placed yet, i.e. we've just
* started!
*/
private OrderType type = null;
/** Price to buy/sell at - default to zero. */
private BigDecimal price = BigDecimal.ZERO;
/** Number of units to buy/sell - default to zero. */
private BigDecimal amount = BigDecimal.ZERO;
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("id", id)
.add("type", type)
.add("price", price)
.add("amount", amount)
.toString();
}
}
}