diff --git a/Example-Script-Crossover-Trader.md b/Example-Script-Crossover-Trader.md new file mode 100644 index 0000000..4e40873 --- /dev/null +++ b/Example-Script-Crossover-Trader.md @@ -0,0 +1,293 @@ +# Crossover Trader (V1.2) + +This is meant as a full example that uses many of the script engine's features. This implementation will be updated as new features are added to the script engine. + +Crossover Trader is a simple automatic trader that buys or sells the selected trading pair when two moving averages of different lengths cross eachother. Crossover Trader is a simple buy/sell automatic trader based either Simple Moving Averages [SMA](https://www.investopedia.com/terms/s/sma.asp) or Exponential Moving Averages [EMA](https://www.investopedia.com/terms/e/ema.asp). + +In the chart below the SMA(12) and SMA(26) are plotted (technically its SMMA in the image, but its just for illustration purpose), and the crossover points indicate where the Crossover Trader would place trades. +![image](https://github.com/AwooOOoo/orko-wiki/blob/master/images/CrossoverTrader-BuySell.png) + +You specify the lengths of the fast (i.e SMA(12)) and slow averages (SMA(26)) and the interval at which to sample the price. + +For instance + +| Parameter | Value | +|------------|-------| +|interval | 5 | +|algorithm | SMA | +|fast average| 12 | +|slow average| 26 | + +In the example above the current price will be sampled every 5 minutes an each of these samples will be fed to the slow and fast simple moving averages (SMA). Since the fast moving average has a smaller length it is more responsive to changes in the market and this moves 'faster'. If the fast average crosses above the slow average a 'BUY' order is issued since the market is trending upwards. If the fast average crosses below the slow averages a 'SELL' order is issued since the market is trending downwards. The orders are currently set as limit orders at the current price so they should (partially) imitate market orders. + +The algorithm will continue trading until cancelled. + +### Engine Features Used + +Here are the features of the script engine that are used and for what + +- setInterval - Used to create its own periodic ticker (set from initial configuration) from the last trade (setTick) data +- setTick - Used to get a stream of trades for the selected trading pair +- setBalance - Used to know what balances are available for the base and quote side of the selected trading pair +- setOpenOrders - Used to determine is we have an order pending on the exchange + +### Input Parameters +The following are the input parameters for the script. If you are going to use this script please create these variables. + +| Name | Description | Default | Required | +|--------|--------------|----------|-----------| +| intervalSize | Interval (in minutes) to to take each sample | 5 | Yes | +| fastAverageLength | Length of fast average. Needs to be smaller than slow average.| 12 | Yes | +| slowAverageLength | Length of slow average. Needs to be larger than fast average | 26 | Yes | +| algorithm | Algorithm for Moving Average (SMA or EMA) | EMA | Yes | + +The _intervalSize_ defines an interval at which a ticker is created internally to tracker the price at the defined interval. The _fastAverageLength_ and _slowAverageLength_ define the number of ticks from the ticker defined by the _intervalSize_. Since there is no current access to history data in the script it you need to wait for the moving averages to fill up before they can be used. This time is the _intervalSize_ * _[fast|slow]AverageLength_ so since the defaults are 5, 12 & 26, this means you need to wait 5 minutes * 12 samples for the fast average and 5 minutes * 26 samples for the slow average so be careful what you set these to as it will take a while to be ready. + +Here is Crossover Trader configuration with the default values. + +![image](https://github.com/AwooOOoo/orko-wiki/blob/master/images/CrossoverTraderMenu.png) + +Here is what you can expect from the Server Notifications. The trades themselves are highlighted and updates to the SMA values are provided at each interval. + +![image](https://user-images.githubusercontent.com/17175274/90343512-3666e400-e011-11ea-9c7c-601a62a38c7c.png) + +### Code +``` +var lastPrice; // The last price data received +var subscriptionTicker; // Subscription for Trade updates +var subscriptionBalance; // Subscription for Balance updates +var interval; // Interval Object +var lastEMA = {}; // The last EMA calculated + +var quotes = []; // Contains arrays of historical data to calculate the Averages +var balances = {}; // Contains balance updates +var pendingOrders; // Whether we have pending orders +var debug = false; // Notifies with extra information if true +var version = 1.2; // Version of this trader + +/* +Initiates a trade + +Improvement: Track Trade execution +*/ +function trade(side) { + var selected = parameters.selectedCoin; + + var params = { + market: parameters.selectedCoin, + price: lastPrice + }; + + if (side === "BUY") { + params.direction = BUY, // BUY is not quoted so I can't use a variable + params.amount = balances[selected.counter] / lastPrice + } else { + params.direction = SELL, // SELL is not quoted so I can't use a variable + params.amount = balances[selected.base]; + } + + notifications.info("Trade: " + side + "ing " + params.amount + " for " + lastPrice); + + // Place the order on the exchange + trading.limitOrder(params); +} + +/* +Calculates a simple moving average (SMA) + +Improvement: When #985 is addressed we won't have to manually create a SMA function +*/ +function simpleMovingAverage(averageLength) { + var sma = 0; + + + if (quotes.length < averageLength) { + if (parameters.algorithm === "SMA") { // Hide text if called from EMA + notifications.info("Filling the SMA(" + averageLength + "): " + quotes.length + "/" + averageLength); + } + sma = undefined; + } else { + // Work from the end as this may be the fast average and doesn't need all the samples + for(c = quotes.length - averageLength; c < quotes.length; c++) { sma += quotes[c]; } + sma /= averageLength; + + if (parameters.algorithm === "SMA") { // Hide text if called from EMA + notifications.info("SMA(" + averageLength + ") = " + parseFloat(sma).toFixed(2)); + } + } + + return sma; +} + +/* +Calculates an exponential moving average (EMA) + +Improvement: When #985 is addressed we won't have to manually create an EMA function +*/ +function exponentialMovingAverage(averageLength) { + var ema = undefined; // Default, buffer still filling + + if (quotes.length <= averageLength && lastEMA[averageLength] == undefined) { + // Initialize the EMA with an SMA + notifications.info("Filling the EMA(" + averageLength + "): " + quotes.length + "/" + averageLength) + + if (quotes.length == averageLength) { // Wait until buffer is full + ema = simpleMovingAverage(averageLength); + lastEMA[averageLength] = ema; + notifications.info("EMA(" + averageLength + ") initially set to: " + ema); + } + + } else if (quotes.length >= averageLength) { // Array is full + // Calculate EMA + + var multiplier = (2 / (averageLength + 1)); + ema = (lastPrice - lastEMA[averageLength]) * multiplier + lastEMA[averageLength]; + lastEMA[averageLength] = ema; + + notifications.info("EMA(" + averageLength + ") = " + parseFloat(ema).toFixed(2)); + } + + return ema; +} + + +/* +Update the moving averages and check if conditions to trade have been met + +Improvement: When we can load history, we won't have to wait before we can trade +*/ +function updateAveragesAndCheckConditions() { + + // Update the historic quote data + if (quotes.length < parameters.slowAverageLength) { // Since it is bigger than the fast + quotes.push(lastPrice); // Buffer is not yet full, push more + } else { + quotes.shift(); // Buffer full, make some room + quotes.push(lastPrice); + } + + // Update the moving averages for the selected algorithm + var fast, slow; + if (parameters.algorithm === "EMA") { + fast = exponentialMovingAverage(parameters.fastAverageLength); + slow = exponentialMovingAverage(parameters.slowAverageLength); + } else { + fast = simpleMovingAverage(parameters.fastAverageLength); + slow = simpleMovingAverage(parameters.slowAverageLength); + } + + // Determine the trade state (do we own more of the base currency of the quote currency + var selected = parameters.selectedCoin; + var baseValue = balances[selected.base] * lastPrice; + var quoteValue = balances[selected.counter]; + + // Provided we have valid balances and aren't currently waiting on a trade to complete + if (baseValue !== undefined && quoteValue !== undefined && pendingOrders.length === 0) { + + // Determine if we are in the trade already + var inTrade = false; + if (baseValue > quoteValue) inTrade = true; + if (debug === true) notifications.info("inTrade:" + inTrade + " - base value: " + baseValue + " quote value: " + quoteValue); + + // Don't allow trades until the averages can be fully computed (i.e. are defined) + if (slow !== undefined && fast !== undefined) { + if (fast > slow && inTrade === false) trade("BUY"); + else if (fast < slow && inTrade === true) trade("SELL"); + } + } +} + +/* +Executes when the script is started +*/ +function start() { + notifications.info("Crossover Trader starting... v" + parseFloat(version).toFixed(2)); + + // Initialize the lastEMA hash if we are using EMA + if (parameters.algorithm === "EMA") { + var slowLength = parameters.slowAverageLength; + var fastLength = parameters.fastAverageLength; + if (!(slowLength in lastEMA)) lastEMA[slowLength] = undefined; + if (!(fastLength in lastEMA)) lastEMA[fastLength] = undefined; + } + + // Check our lengths are the right way around or the trades will be backwards + if (fastLength >= slowLength) { + notifications.alert("The fast average needs to be smaller than the slow average. Aborting!!!"); + return FAILURE_PERMANENT + } + + // Setup a time to trigger at the requested interval + interval = setInterval( + function(event) { + updateAveragesAndCheckConditions(); + }, + Math.round(1000 * 60 * parameters.intervalSize) + ) + + // Subscribe to the ticker / trades of the selected trade pair + subscriptionTicker = events.setTick( + function(event) { + lastPrice = event.ticker().getLast(); + }, + parameters.selectedCoin + ) + + // Subscribe to the balances of the selected trade pair + var subscriptionBalance = events.setBalance( + function(event) { + var b = event.balance(); + + balances[b.currency] = b.available; + if (debug === true) notifications.info("Balance Update:" + b.currency + " = " + b.available); + + }, + parameters.selectedCoin + ); + + var subscriptionOpenOrders = events.setOpenOrders( + function(event) { + pendingOrders = event.openOrders().getOpenOrders(); + }, + parameters.selectedCoin + ); + + return RUNNING +} + +/* +Executes when the script is stopped +*/ +function stop() { + events.clear(subscriptionTicker); // Stop the subscription to the ticker + events.clear(subscriptionBalance); // Stop the subscription to the balance(s) + events.clear(subscriptionOpenOrders); // Stop the subscription to the order order(s) + clearInterval(interval); // Clear the interval +} +``` + +### Potential Future Improvements + +- Add the ability to pre-load history so you don't need to wait for averages to be filled +- Add the ability to call SMA, EMA (and other) algorithms directly from a financial package like in the chart window instead of +having to reimplement them. +- Offer Market orders so fullfillment is guarenteed +- Loss Protection - Limit amount of initial capital that can be lost, otherwise exit algorithm +- Investment Limitation - Limit amount of counter/quote currency that can be used for trading +- Make Algorithm type dropdown selectable instead of being typed in + +### Known Issues + +- Cancelling the job still leaves subscriptions running (engine problem or script problem?) + +### Version History + +| Version | Changes | +|----------|----------| +| 1.0 | EMA Support - Added support for Exponential Moving Average | +| | InTrade Detection - Checks the balances of both currencies of the selected trading pair to determine which side of the trade you are on | +| 1.1 | Balance Support - Looks at your balance of each currency in the selected trading pair to see what funds are available | +| 1.2 | SMA Support (Fixed amount traded, requires manual initial inTrade status as Parameter) | + +### Disclaimer +> Trading cryptocurrencies (or any other financial market) involves substantial risk, and there is always the potential for loss. Your trading results may vary. No representation is being made that this algorithm will guarantee profits, or not result in losses from trading. diff --git a/Scripting.md b/Scripting.md index 7ec16c8..a4d6b57 100644 --- a/Scripting.md +++ b/Scripting.md @@ -107,19 +107,6 @@ var interval = setInterval(function() {}, milliseconds) // Stop the callback (use in stop() method) clearInterval(interval) -// Callback on a price change. Here you have access to: -// ticker.open -// ticker.last -// ticker.bid -// ticker.ask -// ticker.high -// ticker.low -// ticker.vwap -// ticker.volume -// ticker.quoteVolume -// ticker.timestamp -// ticker.bidSize -// ticker.askSize // Although note that many exchanges do not provide all this information. // The tickerSpec type is the same as used above, e.g. { base: "BTC", counter: "USD", exchange: "bitfinex" } events.setTick(function(tickerSpec, ticker) {}, tickerSpec) @@ -147,7 +134,264 @@ var orderId = trading.limitOrder({ price: 6000, amount: 0.1 }) + +``` + +### Ticker + +You can access the current price information (last trade) for a given trading pair. + +``` +balance.setTick(response, tradingPair); +``` + +#### Fields Available +The ticker exposes the following fields; + +``` +currencyPair // Trading pair update is for +open // Opening price of current interval +last // Last sale price +bid // Current Bid price +ask // Current Ask price +high // High price of current interval +low // Low Price of current interval +avg // Average price during current interval +volume // Current volume in last 24hrs on this trading pair +quoteVolume // Volume traded in current interval +timestamp // Date/time stamp of this update +bidSize // Size of the current best Bid price +askSize // Size of the current best Ask price +``` + +#### Complete Example +The following example gets the ticker of the currently selected exchange / trading pair. + +``` +var subscription +var count = 0 + +function start() { + subscription = events.setTick( + function(event) { + var t = event.ticker(); + notifications.info("Price is " + t.currencyPair + " - Bid: " + t.bid + " Ask: " + t.ask); + if (count++ >= 5) { + control.done() + } + }, + parameters.selectedCoin + ) + return RUNNING +} + +function stop() { + events.clear(subscription) +} +``` + +#### Example Output: +Price is ETH/EUR - Bid: 347.55000000 Ask: 348.11000000 +Price is ETH/EUR - Bid: 347.55000000 Ask: 348.11000000 +Price is ETH/EUR - Bid: 347.55000000 Ask: 348.11000000 +Price is ETH/EUR - Bid: 347.55000000 Ask: 348.12000000 +Price is ETH/EUR - Bid: 347.55000000 Ask: 348.12000000 + + +### Account Balance + +You can access the balance information for a given trading pair. + +``` +balance.setBalance(response, tradingPair); +``` + +Note that the *response* is called twice, once for the base and once for the quote. Assuming you had ETH/EUR selected, in the example below, it would call the function once for ETH and once for EUR. + + +#### Fields Available +The balance exposes the following fields; + +``` +currency // Currency type for this balance (i.e. ETH) +total // Total Balance +available // Available Balance +frozen // Frozen Balance +borrowed // Borrowed Balance +loaned // Loaned Balance +withdrawing // Withdrawing Balance +depositing // Depositing Balance +``` + +#### Complete Example +The following example gets the balance of the currently selected exchange / trading pair and displays the currency and available balance. + +``` +var count = 0; + +function start() { + var subscription = events.setBalance( + function(event) { + notifications.info(++count + " Balance: " + event.balance().currency + " - " + event.balance().available); + + }, + parameters.selectedCoin + ); + + return RUNNING +} + +function stop() { + events.clear(subscription); // Stop the subscription +} +``` + +#### Example Output: +> Balance: ETH - 1000 +> Balance: EUR - 1000 + +### Open Orders + +You can access the open orders information for a given trading pair. + +``` +events.setOpenOrders(response, tradingPair); +``` + +#### Fields Available +The open orders exposes the following fields; +[order=LimitOrder [limitPrice=360, Order [type=ASK, originalAmount=10, cumulativeAmount=0, averagePrice=0, fee=0, instrument=ETH/EUR, id=1, timestamp=Thu Aug 20 16:08:14 CEST 2020, status=NEW, flags=[], userReference=175420665]] + +#### Complete Example +The following example gets the balance of the currently selected exchange / trading pair and displays the currency and available balance. + +``` +var count = 0; + +function start() { + var subscription = events.setOpenOrders( + function(event) { + notifications.info("*** OPEN ORDERS: " + event.openOrders()); + + if (count++ >= 1) { + control.done() + } + }, + parameters.selectedCoin + ); + + return RUNNING +} + +function stop() { + events.clear(subscription); // Stop the subscription +} +``` + +#### Example Output: +> Open orders: +> [order=LimitOrder [limitPrice=360, Order [type=ASK, originalAmount=10, cumulativeAmount=0, averagePrice=0, fee=0, instrument=ETH/EUR, id=1, timestamp=Thu Aug 20 16:08:14 CEST 2020, status=NEW, flags=[], userReference=175420665]]] +> [order=LimitOrder [limitPrice=370, Order [type=ASK, originalAmount=10, cumulativeAmount=0, averagePrice=0, fee=0, instrument=ETH/EUR, id=2, timestamp=Thu Aug 20 16:08:19 CEST 2020, status=NEW, flags=[], userReference=154148732]]] + +### Order Book + +You can access the order book information for a given trading pair. + +``` +events.setOrderBook(response, tradingPair); +``` + +#### Fields Available +The order book exposes the following fields within bids and asks; + +``` +bids // Current list of bids +asks // Current list of asks +``` + +More details +> limitPrice=347.77000000, Order [type=BID, originalAmount=0.32508000, cumulativeAmount=null, averagePrice=null, fee=null, instrument=ETH/EUR, id=, timestamp=Thu Aug 20 15:12:35 CEST 2020, status=null, flags=[], userReference=192475211]], LimitOrder [limitPrice=347.75000000, Order [type=BID, originalAmount=0.16254000, cumulativeAmount=null, averagePrice=null, fee=null, instrument=ETH/EUR, id=, timestamp=Thu Aug 20 15:12:37 CEST 2020, status=null, flags=[], userReference=113477373] + +#### Complete Example +The following example gets the order book of the currently selected exchange / trading pair. + +``` +var count = 0; + +function start() { + var subscription = events.setOrderBook( + function(event) { + notifications.info("*** OrderBook BIDS: " + event.orderBook().bids); + notifications.info("*** OrderBook ASKS: " + event.orderBook().asks); + + if (count++ >= 1) { + control.done() + } + }, + parameters.selectedCoin + ); + + return RUNNING +} + +``` + +#### Example Output: +[LimitOrder [limitPrice=347.77000000, Order [type=BID, originalAmount=0.32508000, cumulativeAmount=null, averagePrice=null, fee=null, instrument=ETH/EUR, id=, timestamp=Thu Aug 20 15:12:35 CEST 2020, status=null, flags=[], userReference=192475211]], LimitOrder [limitPrice=347.75000000, Order [type=BID, originalAmount=0.16254000, cumulativeAmount=null, averagePrice=null, fee=null, instrument=ETH/EUR, id=, timestamp=Thu Aug 20 15:12:37 CEST 2020, status=null, flags=[], userReference=113477373]], LimitOrder [limitPrice=347.56000000, Order [type=BID, originalAmount=1.07717000, cumulativeAmount=null, averagePrice=null, fee=null, instrument=ETH/EUR, id=, timestamp=Thu Aug 20 15:12:26 CEST 2020, status=null, flags=[], userReference=155455028]], LimitOrder [limitPrice=347.38000000, Order [type=BID, originalAmount=2.69161000, cumulativeAmount=null, averagePrice=null, fee=null, instrument=ETH/EUR, id=null, timestamp=null, status=null, flags=[], userReference=115320557]] ... + +### User Trades + +You can access the user trades information for a given trading pair. + +``` +events.setUserTrades(response, tradingPair); +``` + +#### Fields Available +User Trades exposes the following fields; + +``` +type // type of trade (BID / ASK) +originalAmount // Amount to be traded +currencyPair // trading pair (i.e ETH/EUR) +price // price bid / asked +timestamp // time/date of transaction (i.e. Fri Aug 21 12:48:59 CEST 2020) +id // Identifier +orderId // Unique identifier for the trade i.e. '12345678' +feeAmount // Transaction Fee amount +feeCurrency // Curreny 'feeAmount' is in (i.e. 'ETH') +orderUserReference // User defined identifier for the transaction (i.e. 'null') +``` + +#### Complete Example +The following example gets the User trades of the currently selected exchange / trading pair. + ``` +var count = 0; + +function start() { + var subscription = events.setUserTrades( + function(event) { + notifications.info("*** USER TRADES: " + event.trade()); + + if (count++ >= 1) { + control.done() + } + }, + parameters.selectedCoin + ); + + return RUNNING +} +``` + +#### Example Output: +UserTrade[type=BID, originalAmount=1, currencyPair=ETH/EUR, price=346.10000000, timestamp=Fri Aug 21 12:48:59 CEST 2020, id=1, orderId='1', feeAmount=0, feeCurrency='ETH', orderUserReference='null']' + + + + + ### Parameters diff --git a/_Sidebar.md b/_Sidebar.md index 54663ae..141e80b 100644 --- a/_Sidebar.md +++ b/_Sidebar.md @@ -17,6 +17,7 @@ 1. [User Guide](User-Guide) 1. [Scripting](Scripting) 1. [Script examples](Script-Library) + 1. [Simple Crossover Trader](Example-Script-Crossover-Trader) 1. [Supporting the project](Supporting-The-Project) 1. [Contributing](Contributing) 1. [Developer Guide](Developer-guide) diff --git a/images/CrossoverTrader-BuySell.png b/images/CrossoverTrader-BuySell.png new file mode 100644 index 0000000..bcc6630 Binary files /dev/null and b/images/CrossoverTrader-BuySell.png differ diff --git a/images/CrossoverTraderMenu.png b/images/CrossoverTraderMenu.png new file mode 100644 index 0000000..feed3f6 Binary files /dev/null and b/images/CrossoverTraderMenu.png differ