├── .eslintrc ├── .eslintrc.json ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── docs ├── .keep ├── accumulate_distribute.md ├── ao_host.md ├── helpers.md ├── iceberg.md └── twap.md ├── examples └── ao_host.js ├── index.js ├── jsdoc.conf.json ├── lib ├── accumulate_distribute │ ├── events │ │ ├── data_managed_book.js │ │ ├── data_managed_candles.js │ │ ├── data_trades.js │ │ ├── error_minimum_size.js │ │ ├── insufficient_balance.js │ │ ├── life_start.js │ │ ├── life_stop.js │ │ ├── orders_order_cancel.js │ │ ├── orders_order_fill.js │ │ ├── self_interval_tick.js │ │ └── self_submit_order.js │ ├── index.js │ ├── meta │ │ ├── declare_channels.js │ │ ├── declare_events.js │ │ ├── gen_order_label.js │ │ ├── gen_preview.js │ │ ├── get_ui_def.js │ │ ├── init_state.js │ │ ├── process_params.js │ │ ├── serialize.js │ │ ├── unserialize.js │ │ └── validate_params.js │ └── util │ │ ├── gen_order_amounts.js │ │ ├── generate_order.js │ │ ├── has_indicator_cap.js │ │ ├── has_indicator_offset.js │ │ ├── has_ob_requirement.js │ │ ├── has_trade_requirement.js │ │ └── schedule_tick.js ├── ao_host.js ├── async_event_emitter.js ├── define_algo_order.js ├── errors │ └── no_data.js ├── host │ ├── events │ │ ├── assign_channel.js │ │ ├── cancel_all_orders.js │ │ ├── insufficient_balance.js │ │ ├── minimum_size_error.js │ │ ├── notify.js │ │ ├── submit_all_orders.js │ │ └── update_state.js │ ├── gen_helpers.js │ ├── init_ao.js │ ├── init_ao_state.js │ ├── ui │ │ └── register_ao_uis.js │ ├── with_ao_update.js │ └── ws2 │ │ ├── bind_bus.js │ │ └── process_message.js ├── iceberg │ ├── events │ │ ├── error_minimum_size.js │ │ ├── insufficient_balance.js │ │ ├── life_start.js │ │ ├── life_stop.js │ │ ├── orders_order_cancel.js │ │ ├── orders_order_fill.js │ │ └── self_submit_orders.js │ ├── index.js │ ├── meta │ │ ├── declare_events.js │ │ ├── gen_order_label.js │ │ ├── gen_preview.js │ │ ├── get_ui_def.js │ │ ├── init_state.js │ │ ├── process_params.js │ │ ├── serialize.js │ │ ├── unserialize.js │ │ └── validate_params.js │ └── util │ │ └── generate_orders.js ├── ping_pong │ ├── events │ │ ├── error_minimum_size.js │ │ ├── insufficient_balance.js │ │ ├── life_start.js │ │ ├── life_stop.js │ │ ├── orders_order_cancel.js │ │ └── orders_order_fill.js │ ├── index.js │ ├── meta │ │ ├── gen_order_label.js │ │ ├── gen_preview.js │ │ ├── get_ui_def.js │ │ ├── init_state.js │ │ ├── process_params.js │ │ ├── serialize.js │ │ ├── unserialize.js │ │ └── validate_params.js │ └── util │ │ └── gen_ping_pong_table.js ├── testing │ └── create_harness.js ├── twap │ ├── config.js │ ├── events │ │ ├── data_managed_book.js │ │ ├── data_trades.js │ │ ├── error_minimum_size.js │ │ ├── insufficient_balance.js │ │ ├── life_start.js │ │ ├── life_stop.js │ │ ├── orders_order_cancel.js │ │ ├── orders_order_fill.js │ │ └── self_interval_tick.js │ ├── index.js │ ├── meta │ │ ├── declare_channels.js │ │ ├── declare_events.js │ │ ├── gen_order_label.js │ │ ├── gen_preview.js │ │ ├── get_ui_def.js │ │ ├── init_state.js │ │ ├── process_params.js │ │ ├── serialize.js │ │ ├── unserialize.js │ │ └── validate_params.js │ └── util │ │ ├── generate_order.js │ │ ├── get_ob_price.js │ │ ├── get_trade_price.js │ │ ├── has_ob_target.js │ │ ├── has_trade_target.js │ │ └── is_target_met.js └── util │ └── has_open_orders.js ├── package.json └── test ├── iceberg ├── events │ ├── life_start.js │ ├── life_stop.js │ ├── orders_order_cancel.js │ ├── orders_order_fill.js │ └── self_submit_orders.js ├── index.js ├── meta │ ├── init_state.js │ ├── process_params.js │ └── validate_params.js └── util │ └── generate_orders.js └── twap ├── events ├── data_managed_book.js ├── data_trades.js ├── life_start.js ├── life_stop.js ├── orders_order_cancel.js ├── orders_order_fill.js └── self_interval_tick.js └── meta ├── init_state.js ├── process_params.js └── validate_params.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard" 3 | } -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": [ 4 | "airbnb" 5 | ], 6 | "rules": { 7 | "jsx-quotes": ["error", "prefer-single"], 8 | "semi": [1, "never"], 9 | "max-len": 0, 10 | "import/no-named-as-default": 0, 11 | "import/no-named-as-default-member": 0 12 | }, 13 | "settings": { 14 | "import/resolver": { 15 | "node": { 16 | "moduleDirectory": [ 17 | "node_modules", 18 | "lib" 19 | ] 20 | } 21 | } 22 | }, 23 | "env": { 24 | "browser": true, 25 | "jest": true 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | dist/* 3 | node_modules 4 | npm-debug.log 5 | .vscode 6 | *.swo 7 | *.swp 8 | .DS_Store 9 | yarn-error.log 10 | coverage 11 | package-lock.json 12 | .env 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: node_js 4 | node_js: 5 | - "stable" 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /docs/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mr-dev-dragon/bfx-hf-algo/dcdfba1dcf26d77d7af075df05e2be7a6882e66b/docs/.keep -------------------------------------------------------------------------------- /docs/accumulate_distribute.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Accumulate/Distribute 4 | Accumulate/Distribute allows you to break up a large order into smaller 5 | randomized chunks, submitted at regular or irregular intervals to minimise 6 | detection by other players in the market. 7 | 8 | By enabling the 'Await Fill' option, the algorithm will ensure each 9 | component fills before submitting subsequent orders. Enabling the 'Catch Up' 10 | flag will cause the algorithm to ignore the slice interval for the next order 11 | if previous orders have taken longer than expected to fill, thereby ensuring 12 | the time-to-fill for the entire order is not adversely affected. 13 | 14 | The price must be manually specified as `limitPrice` for `LIMIT` order types, 15 | or as a combination of a price offset & cap for `RELATIVE` order types. 16 | `MARKET` A/D orders execute using `MARKET` atomic orders, and offer no price 17 | control. 18 | 19 | For `RELATIVE` A/D orders, the price offset & cap can both be set to one of 20 | the following: 21 | * Top ask 22 | * Top bid 23 | * Orderbook mid price 24 | * Last trade price 25 | * Moving Average (configurable period, time frame, candle price) 26 | * Exponential Moving Average (configurable period, time frame, candle price) 27 | 28 | The period limit for moving average targets/caps is `240`, being the number 29 | of candles returned by the Bitfinex API when subscribing to a candle data 30 | channel. 31 | 32 | **Kind**: global variable 33 | 34 | | Param | Type | Description | 35 | | --- | --- | --- | 36 | | symbol | string | symbol to trade on | 37 | | amount | number | total order amount | 38 | | sliceAmount | number | individual slice order amount | 39 | | sliceInterval | number | delay in ms between slice orders | 40 | | intervalDistortion | number | slice interval distortion in %, default 0 | 41 | | amountDistortion | number | slice amount distortion in %, default 0 | 42 | | orderType | string | LIMIT, MARKET, RELATIVE | 43 | | limitPrice | number | price for LIMIT orders | 44 | | catchUp | boolean | if true, interval will be ignored if behind with filling slices | 45 | | awaitFill | boolean | if true, slice orders will be kept open until filled | 46 | | relativeOffset | Object | price reference for RELATIVE orders | 47 | | relativeOffset.type | string | ask, bid, mid, last, ma, or ema | 48 | | relativeOffset.delta | number | offset distance from price reference | 49 | | relativeOffset.args | Array.<number> | MA or EMA indicator arguments [period] | 50 | | relativeOffset.candlePrice | string | 'open', 'high', 'low', 'close' for MA or EMA indicators | 51 | | relativeOffset.candleTimeFrame | string | '1m', '5m', '1D', etc, for MA or EMA indicators | 52 | | relativeCap | Object | maximum price reference for RELATIVE orders | 53 | | relativeCap.type | string | ask, bid, mid, last, ma, or ema | 54 | | relativeCap.delta | number | cap distance from price reference | 55 | | relativeCap.args | Array.<number> | MA or EMA indicator arguments [period] | 56 | | relativeCap.candlePrice | string | 'open', 'high', 'low', 'close' for MA or EMA indicators | 57 | | relativeCap.candleTimeFrame | string | '1m', '5m', '1D', etc, for MA or EMA indicators | 58 | | _margin | boolean | if false, order type is prefixed with EXCHANGE | 59 | 60 | -------------------------------------------------------------------------------- /docs/ao_host.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## AOHost 4 | The AOHost class provides a wrapper around the algo order system, and 5 | manages lifetime events/order execution. Internally it hosts a Manager 6 | instance from bfx-api-node-core for communication with the Bitfinex API, and 7 | listens for websocket stream events in order to update order state/trigger 8 | algo order events. 9 | 10 | Execution is handled by an event system, with events being triggered by 11 | Bitfinex API websocket stream payloads, and the algo orders themselves. 12 | 13 | **Kind**: global class 14 | 15 | * [AOHost](#AOHost) 16 | * [new AOHost(args)](#new_AOHost_new) 17 | * [.connect()](#AOHost+connect) 18 | * [.getAO(id)](#AOHost+getAO) ⇒ Object 19 | * [.getAOInstance(gid)](#AOHost+getAOInstance) ⇒ Object 20 | * [.loadAllAOs()](#AOHost+loadAllAOs) 21 | * [.loadAO(id, gid, loadedState)](#AOHost+loadAO) ⇒ string 22 | * [.startAO(id, args)](#AOHost+startAO) ⇒ string 23 | * [.stopAO(gid)](#AOHost+stopAO) 24 | 25 | 26 | 27 | ### new AOHost(args) 28 | 29 | | Param | Type | Description | 30 | | --- | --- | --- | 31 | | args | Object | | 32 | | args.apiKey | string | | 33 | | args.apiSecret | string | | 34 | | args.wsURL | string | wss://api.bitfinex.com/ws/2 | 35 | | args.restURL | string | https://api.bitfinex.com | 36 | | args.agent | Object | optional proxy agent | 37 | | args.aos | Array.<Object> | algo orders to manage | 38 | 39 | 40 | 41 | ### aoHost.connect() 42 | Opens a new socket connection on the internal socket manager 43 | 44 | **Kind**: instance method of [AOHost](#AOHost) 45 | 46 | 47 | ### aoHost.getAO(id) ⇒ Object 48 | Returns the algo order definition identified by the provided ID 49 | 50 | **Kind**: instance method of [AOHost](#AOHost) 51 | **Returns**: Object - aoDef 52 | 53 | | Param | Type | Description | 54 | | --- | --- | --- | 55 | | id | string | i.e. bfx.iceberg | 56 | 57 | 58 | 59 | ### aoHost.getAOInstance(gid) ⇒ Object 60 | Returns the active AO instance state identified by the provided GID 61 | 62 | **Kind**: instance method of [AOHost](#AOHost) 63 | **Returns**: Object - state - algo order state 64 | 65 | | Param | Type | Description | 66 | | --- | --- | --- | 67 | | gid | string | algo order group ID | 68 | 69 | 70 | 71 | ### aoHost.loadAllAOs() 72 | Loads and starts all saved previously active algo orders 73 | 74 | **Kind**: instance method of [AOHost](#AOHost) 75 | 76 | 77 | ### aoHost.loadAO(id, gid, loadedState) ⇒ string 78 | Loads and starts a single algo order, with the provided serialized state 79 | 80 | **Kind**: instance method of [AOHost](#AOHost) 81 | **Returns**: string - gid 82 | 83 | | Param | Type | Description | 84 | | --- | --- | --- | 85 | | id | string | algo order definition ID | 86 | | gid | string | algo order instance group ID | 87 | | loadedState | Object | algo order instance state | 88 | 89 | 90 | 91 | ### aoHost.startAO(id, args) ⇒ string 92 | Creates and starts a new algo order instance, based on the AO def 93 | identified by the supplied ID 94 | 95 | **Kind**: instance method of [AOHost](#AOHost) 96 | **Returns**: string - gid - instance GID 97 | 98 | | Param | Type | Description | 99 | | --- | --- | --- | 100 | | id | string | algo order definition ID, i.e. bfx.iceberg | 101 | | args | Object | algo order arguments/parameters | 102 | 103 | 104 | 105 | ### aoHost.stopAO(gid) 106 | Stops an algo order instance by GID 107 | 108 | **Kind**: instance method of [AOHost](#AOHost) 109 | 110 | | Param | Type | Description | 111 | | --- | --- | --- | 112 | | gid | string | algo order instance GID | 113 | 114 | -------------------------------------------------------------------------------- /docs/helpers.md: -------------------------------------------------------------------------------- 1 | ## Functions 2 | 3 |
4 |
debug(str, ...args)
5 |

Logs a string to the console, tagged by AO id/gid

6 |
7 |
emitSelf(eventName, ...eventArgs)
8 |

Triggeres an event on the 'self' section

9 |
10 |
emitSelfAsync(eventName, ...eventArgs)
11 |

Like emitSelf but operates after a timeout

12 |
13 |
emit(eventName, ...eventArgs)
14 |

Triggers a generic event

15 |
16 |
emitAsync(eventName, ...eventArgs)
17 |

Like emit but operates after a timeout

18 |
19 |
notifyUI(level, message)
20 |

Triggers an UI notification, sent out via the active websocket connection

21 |
22 |
cancelOrderWithDelay(state, delay, order)Object
23 |

Cancels the provided order after a delay, and removes it from the active 24 | order set.

25 |
26 |
cancelAllOrdersWithDelay(state, delay)Object
27 |

Cancels all orders currently on the AO state after the specified delay

28 |
29 |
submitOrderWithDelay(state, delay, order)Object
30 |

Submits an order after a delay, and adds it to the active order set on 31 | the AO state.

32 |
33 |
declareEvent(instance, aoHost, eventName, path)
34 |

Hooks up the listener for a new event on the 'self' section

35 |
36 |
declareChannel(instance, aoHost, channel, filter)Object
37 |

Assigns a data channel to the provided AO instance

38 |
39 |
updateState(instance, update)Object
40 |

Updates the state for the provided AO instance

41 |
42 |
43 | 44 | 45 | 46 | ## debug(str, ...args) 47 | Logs a string to the console, tagged by AO id/gid 48 | 49 | **Kind**: global function 50 | 51 | | Param | Type | 52 | | --- | --- | 53 | | str | string | 54 | | ...args | any | 55 | 56 | 57 | 58 | ## emitSelf(eventName, ...eventArgs) 59 | Triggeres an event on the 'self' section 60 | 61 | **Kind**: global function 62 | 63 | | Param | Type | 64 | | --- | --- | 65 | | eventName | string | 66 | | ...eventArgs | any | 67 | 68 | **Example** 69 | ```js 70 | await emitSelf('submit_orders') 71 | ``` 72 | 73 | 74 | ## emitSelfAsync(eventName, ...eventArgs) 75 | Like `emitSelf` but operates after a timeout 76 | 77 | **Kind**: global function 78 | 79 | | Param | Type | 80 | | --- | --- | 81 | | eventName | string | 82 | | ...eventArgs | any | 83 | 84 | 85 | 86 | ## emit(eventName, ...eventArgs) 87 | Triggers a generic event 88 | 89 | **Kind**: global function 90 | 91 | | Param | Type | 92 | | --- | --- | 93 | | eventName | string | 94 | | ...eventArgs | any | 95 | 96 | **Example** 97 | ```js 98 | await emit('exec:order:submit:all', gid, [order], submitDelay) 99 | ``` 100 | 101 | 102 | ## emitAsync(eventName, ...eventArgs) 103 | Like `emit` but operates after a timeout 104 | 105 | **Kind**: global function 106 | 107 | | Param | Type | 108 | | --- | --- | 109 | | eventName | string | 110 | | ...eventArgs | any | 111 | 112 | 113 | 114 | ## notifyUI(level, message) 115 | Triggers an UI notification, sent out via the active websocket connection 116 | 117 | **Kind**: global function 118 | 119 | | Param | Type | Description | 120 | | --- | --- | --- | 121 | | level | string | 'info', 'success', 'error', 'warning' | 122 | | message | string | notification content | 123 | 124 | **Example** 125 | ```js 126 | await notifyUI('info', `Scheduled tick in ${delay}s`) 127 | ``` 128 | 129 | 130 | ## cancelOrderWithDelay(state, delay, order) ⇒ Object 131 | Cancels the provided order after a delay, and removes it from the active 132 | order set. 133 | 134 | **Kind**: global function 135 | **Returns**: Object - nextState 136 | 137 | | Param | Type | Description | 138 | | --- | --- | --- | 139 | | state | Object | current AO instance state | 140 | | delay | number | in ms | 141 | | order | Order | | 142 | 143 | 144 | 145 | ## cancelAllOrdersWithDelay(state, delay) ⇒ Object 146 | Cancels all orders currently on the AO state after the specified delay 147 | 148 | **Kind**: global function 149 | **Returns**: Object - nextState 150 | 151 | | Param | Type | Description | 152 | | --- | --- | --- | 153 | | state | Object | current AO instance state | 154 | | delay | number | in ms | 155 | 156 | 157 | 158 | ## submitOrderWithDelay(state, delay, order) ⇒ Object 159 | Submits an order after a delay, and adds it to the active order set on 160 | the AO state. 161 | 162 | **Kind**: global function 163 | **Returns**: Object - nextState 164 | 165 | | Param | Type | Description | 166 | | --- | --- | --- | 167 | | state | Object | current AO instance state | 168 | | delay | number | | 169 | | order | Order | | 170 | 171 | 172 | 173 | ## declareEvent(instance, aoHost, eventName, path) 174 | Hooks up the listener for a new event on the 'self' section 175 | 176 | **Kind**: global function 177 | 178 | | Param | Type | Description | 179 | | --- | --- | --- | 180 | | instance | Object | full AO instance, with state/h | 181 | | aoHost | Object | | 182 | | eventName | string | | 183 | | path | string | on the 'self' section | 184 | 185 | **Example** 186 | ```js 187 | declareEvent(instance, host, 'self:interval_tick', 'interval_tick') 188 | ``` 189 | 190 | 191 | ## declareChannel(instance, aoHost, channel, filter) ⇒ Object 192 | Assigns a data channel to the provided AO instance 193 | 194 | **Kind**: global function 195 | **Returns**: Object - nextState 196 | 197 | | Param | Type | Description | 198 | | --- | --- | --- | 199 | | instance | Object | full AO instance, with state/h | 200 | | aoHost | Object | | 201 | | channel | string | channel name, i.e. 'ticker' | 202 | | filter | Object | channel spec, i.e. { symbol: 'tBTCUSD' } | 203 | 204 | **Example** 205 | ```js 206 | await declareChannel(instance, host, 'trades', { symbol }) 207 | ``` 208 | 209 | 210 | ## updateState(instance, update) ⇒ Object 211 | Updates the state for the provided AO instance 212 | 213 | **Kind**: global function 214 | **Returns**: Object - nextState 215 | 216 | | Param | Type | Description | 217 | | --- | --- | --- | 218 | | instance | Object | full AO instance, with state/h | 219 | | update | Object | new state | 220 | 221 | -------------------------------------------------------------------------------- /docs/iceberg.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Iceberg 4 | Iceberg allows you to place a large order on the market while ensuring only 5 | a small part of it is ever filled at once. By enabling the 'Excess As Hidden' 6 | option, it is possible to offer up the remainder as a hidden order, allowing 7 | for minimal market disruption when executing large trades. 8 | 9 | **Kind**: global variable 10 | 11 | | Param | Type | Description | 12 | | --- | --- | --- | 13 | | symbol | string | symbol to trade on | 14 | | amount | number | total order amount | 15 | | sliceAmount | number | iceberg slice order amount | 16 | | sliceAmountPerc | number | optional, slice amount as % of total amount | 17 | | excessAsHidden | boolean | whether to submit remainder as a hidden order | 18 | | orderType | string | LIMIT or MARKET | 19 | | submitDelay | number | in ms, default 1500 | 20 | | cancelDelay | number | in ms, default 5000 | 21 | | _margin | boolean | if false, prefixes order type with EXCHANGE | 22 | 23 | -------------------------------------------------------------------------------- /docs/twap.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## TWAP 4 | TWAP spreads an order out through time in order to fill at the time-weighted 5 | average price, calculated between the time the order is submitted to the 6 | final atomic order close. 7 | 8 | The price can be specified as a fixed external target, such as the top 9 | bid/ask or last trade price, or as an explicit target which must be matched 10 | against the top bid/ask/last trade/etc. 11 | 12 | Available price targets/explicit target conditions: 13 | * OB side price (top bid/ask) 14 | * OB mid price 15 | * Last trade price 16 | 17 | **Kind**: global variable 18 | 19 | | Param | Type | Description | 20 | | --- | --- | --- | 21 | | symbol | string | symbol to trade on | 22 | | amount | number | total order amount | 23 | | sliceAmount | number | individual slice order amount | 24 | | priceDelta | number | max acceptable distance from price target | 25 | | priceCondition | string | MATCH_LAST, MATCH_SIDE, MATCH_MID | 26 | | priceTarget | number \| string | numeric, or OB_SIDE, OB_MID, LAST | 27 | | tradeBeyondEnd | boolean | if true, slices are not cancelled after their interval expires | 28 | | orderType | string | LIMIT or MARKET | 29 | | _margin | boolean | if false, order type is prefixed with EXCHANGE | 30 | | submitDelay | number | in ms, defaults to 1500 | 31 | | cancelDelay | number | in ms, defaults to 5000 | 32 | 33 | -------------------------------------------------------------------------------- /examples/ao_host.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | require('dotenv').config() 4 | 5 | process.env.DEBUG = '*,-bfx:api:ws:on_channel_message' 6 | 7 | const debug = require('debug')('bfx:ao:examples:ao-host') 8 | const AOHost = require('./ao_host') 9 | const Iceberg = require('./iceberg') 10 | const TWAP = require('./twap') 11 | const AccumulateDistribute = require('./accumulate_distribute') 12 | 13 | const { 14 | WS_URL, REST_URL, API_KEY, API_SECRET 15 | } = process.env 16 | 17 | const host = new AOHost({ 18 | aos: [Iceberg, TWAP, AccumulateDistribute], 19 | apiKey: API_KEY, 20 | apiSecret: API_SECRET, 21 | wsURL: WS_URL, 22 | restURL: REST_URL 23 | }) 24 | 25 | host.on('ao:start', (instance) => { 26 | const { state = {} } = instance 27 | const { id, gid } = state 28 | debug('started AO %s [gid %s]', id, gid) 29 | }) 30 | 31 | host.on('ao:stop', (instance) => { 32 | const { state = {} } = instance 33 | const { id, gid } = state 34 | debug('stopped AO %s [gid %s]', id, gid) 35 | }) 36 | 37 | host.on('ws2:auth:error', (packet) => { 38 | debug('error authenticating: %j', packet) 39 | }) 40 | 41 | host.on('error', (err) => { 42 | debug('error: %s', err) 43 | }) 44 | 45 | host.once('ws2:auth:success', async () => { 46 | const gid = await host.startAO('bfx.accumulate_distribute', { 47 | symbol: 'tBTCUSD', 48 | amount: -0.2, 49 | sliceAmount: -0.1, 50 | sliceInterval: 10000, 51 | intervalDistortion: 0.20, 52 | amountDistortion: 0.20, 53 | orderType: 'RELATIVE', // MARKET, LIMIT, RELATIVE 54 | relativeOffset: { type: 'ask', args: [20], delta: -10 }, 55 | relativeCap: { type: 'bid', delta: 10 }, 56 | submitDelay: 150, 57 | cancelDelay: 150, 58 | catchUp: true, // if true & behind, ignore slice interval (after prev fill) 59 | awaitFill: true, 60 | _margin: false 61 | }) 62 | 63 | debug('started AO %s', gid) 64 | 65 | /* 66 | const gid = await host.startAO('bfx.twap', { 67 | symbol: 'tBTCUSD', 68 | amount: -0.5, 69 | sliceAmount: -0.1, 70 | sliceInterval: 10, 71 | priceDelta: 100, 72 | priceTarget: 16650, 73 | priceCondition: TWAP.Config.PRICE_COND.MATCH_LAST, 74 | tradeBeyondEnd: false, 75 | orderType: 'LIMIT', 76 | submitDelay: 150, 77 | cancelDelay: 150, 78 | _margin: false 79 | }) 80 | */ 81 | 82 | /* 83 | const gid = host.startAO('bfx.iceberg', { 84 | symbol: 'tBTCUSD', 85 | price: 21000, 86 | amount: -0.5, 87 | sliceAmount: -0.1, 88 | excessAsHidden: true, 89 | orderType: 'LIMIT', 90 | submitDelay: 150, 91 | cancelDelay: 150, 92 | _margin: false, 93 | }) 94 | */ 95 | }) 96 | 97 | host.connect() 98 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | AOHost: require('./lib/ao_host'), 5 | IcebergOrder: require('./lib/iceberg'), 6 | TWAPOrder: require('./lib/twap'), 7 | AccumulateDistribute: require('./lib/accumulate_distribute'), 8 | PingPong: require('./lib/ping_pong'), 9 | NoDataError: require('./lib/errors/no_data') 10 | } 11 | -------------------------------------------------------------------------------- /jsdoc.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["plugins/markdown"] 3 | } -------------------------------------------------------------------------------- /lib/accumulate_distribute/events/data_managed_book.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const hasOBRequirement = require('../util/has_ob_requirement') 4 | 5 | module.exports = async (instance = {}, book, meta) => { 6 | const { state = {}, h = {} } = instance 7 | const { args = {} } = state 8 | const { symbol } = args 9 | const { debug, updateState } = h 10 | const { chanFilter } = meta 11 | const chanSymbol = chanFilter.symbol 12 | 13 | if (!hasOBRequirement(args) || symbol !== chanSymbol) { 14 | return 15 | } 16 | 17 | debug('recv updated order book for %s', symbol) 18 | 19 | await updateState(instance, { 20 | lastBook: book 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /lib/accumulate_distribute/events/data_managed_candles.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const _reverse = require('lodash/reverse') 4 | const hasIndicatorOffset = require('../util/has_indicator_offset') 5 | const hasIndicatorCap = require('../util/has_indicator_cap') 6 | 7 | module.exports = async (instance = {}, candles, meta) => { 8 | const { state = {}, h = {} } = instance 9 | const { args = {}, offsetIndicator, capIndicator } = state 10 | const { symbol, relativeOffset, relativeCap } = args 11 | const { debug, updateState } = h 12 | const { chanFilter } = meta 13 | const { key } = chanFilter 14 | const chanSymbol = key.split(':')[2] 15 | 16 | if ((!hasIndicatorOffset(args) && !hasIndicatorCap(args)) || symbol !== chanSymbol) { 17 | return 18 | } 19 | 20 | const [ lastCandle ] = candles 21 | 22 | // Both indicators start with 0 length 23 | if ((capIndicator && capIndicator.l() === 0) || (offsetIndicator && offsetIndicator.l() === 0)) { 24 | debug('seeding indicators with %d candle prices', candles.length) 25 | 26 | const orderedCandles = _reverse(candles) 27 | orderedCandles.forEach((candle) => { 28 | if (hasIndicatorCap(args)) { 29 | capIndicator.add(candle[relativeCap.candlePrice]) 30 | } 31 | 32 | if (hasIndicatorOffset(args)) { 33 | offsetIndicator.add(candle[relativeOffset.candlePrice]) 34 | } 35 | }) 36 | } else { // add new data point/update data point 37 | if (hasIndicatorOffset(args)) { 38 | const price = lastCandle[relativeOffset.candlePrice] 39 | 40 | debug('updating relative offset indicator with candle price %f [%j]', price, lastCandle) 41 | 42 | if (!state.lastCandle) { 43 | offsetIndicator.add(price) 44 | } else if (state.lastCandle.mts === lastCandle.mts) { 45 | offsetIndicator.update(price) 46 | } else { 47 | offsetIndicator.add(price) 48 | } 49 | } 50 | 51 | if (hasIndicatorCap(args)) { 52 | const price = lastCandle[relativeCap.candlePrice] 53 | 54 | debug('updating relative cap indicator with candle price %f [%j]', price, lastCandle) 55 | 56 | if (!state.lastCandle) { 57 | capIndicator.add(price) 58 | } else if (state.lastCandle.mts === lastCandle.mts) { 59 | capIndicator.update(price) 60 | } else { 61 | capIndicator.add(price) 62 | } 63 | } 64 | } 65 | 66 | await updateState(instance, { candles }) 67 | } 68 | -------------------------------------------------------------------------------- /lib/accumulate_distribute/events/data_trades.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const hasTradeRequirement = require('../util/has_trade_requirement') 4 | 5 | module.exports = async (instance = {}, trades, meta) => { 6 | const { state = {}, h = {} } = instance 7 | const { args = {} } = state 8 | const { symbol } = args 9 | const { debug, updateState } = h 10 | const { chanFilter } = meta 11 | const chanSymbol = chanFilter.symbol 12 | 13 | if (!hasTradeRequirement(args) || symbol !== chanSymbol) { 14 | return 15 | } 16 | 17 | const [ lastTrade ] = trades 18 | const { price } = lastTrade 19 | 20 | debug('recv last price: %f [%j]', price, lastTrade) 21 | 22 | await updateState(instance, { lastTrade }) 23 | } 24 | -------------------------------------------------------------------------------- /lib/accumulate_distribute/events/error_minimum_size.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * Cancel the AO if we try to submit an order below the minimum size, since it 5 | * means the remaining amount is below the min size (and therefore cannot fill) 6 | * 7 | * @param {Object} instance 8 | * @param {Order} order - order which is below the min size for its symbol 9 | */ 10 | module.exports = async (instance = {}, o) => { 11 | const { state = {}, h = {} } = instance 12 | const { gid, args = {}, orders = {} } = state 13 | const { emit, debug } = h 14 | const { cancelDelay } = args 15 | 16 | debug('received minimum size error for order: %f @ %f', o.amountOrig, o.price) 17 | debug('stopping order...') 18 | 19 | await emit('exec:order:cancel:all', gid, orders, cancelDelay) 20 | await emit('exec:stop') 21 | } 22 | -------------------------------------------------------------------------------- /lib/accumulate_distribute/events/insufficient_balance.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * Cancel the AO if we try to submit an order without having enough balance 5 | * 6 | * @param {Object} instance 7 | * @param {Order} order - order which is below the min size for its symbol 8 | */ 9 | module.exports = async (instance = {}, o) => { 10 | const { state = {}, h = {} } = instance 11 | const { gid, args = {}, orders = {} } = state 12 | const { emit, debug } = h 13 | const { cancelDelay } = args 14 | 15 | debug('received insufficient balance error for order: %f @ %f', o.amountOrig, o.price) 16 | debug('stopping order...') 17 | 18 | await emit('exec:order:cancel:all', gid, orders, cancelDelay) 19 | await emit('exec:stop') 20 | } 21 | -------------------------------------------------------------------------------- /lib/accumulate_distribute/events/life_start.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const HFI = require('bfx-hf-indicators') 4 | const scheduleTick = require('../util/schedule_tick') 5 | const hasIndicatorOffset = require('../util/has_indicator_offset') 6 | const hasIndicatorCap = require('../util/has_indicator_cap') 7 | 8 | module.exports = async (instance = {}) => { 9 | const { state = {}, h = {} } = instance 10 | const { args = {}, orderAmounts, remainingAmount } = state 11 | const { debug, updateState } = h 12 | const { amount, relativeCap, relativeOffset } = args 13 | 14 | debug( 15 | 'starting with order amounts (total %f) %j [rem %f]', 16 | amount, orderAmounts, remainingAmount 17 | ) 18 | 19 | if (hasIndicatorOffset(args)) { 20 | const IndicatorClass = HFI[relativeOffset.type.toUpperCase()] 21 | const offsetIndicator = new IndicatorClass(relativeOffset.args) 22 | 23 | debug('initialized offset indicator %s %j', relativeOffset.type, relativeOffset.args) 24 | 25 | await updateState(instance, { offsetIndicator }) 26 | } 27 | 28 | if (hasIndicatorCap(args)) { 29 | const IndicatorClass = HFI[relativeCap.type.toUpperCase()] 30 | const capIndicator = new IndicatorClass(relativeCap.args) 31 | 32 | debug('initialized cap indicator %s %j', relativeCap.type, relativeCap.args) 33 | 34 | await updateState(instance, { capIndicator }) 35 | } 36 | 37 | await scheduleTick(instance) 38 | } 39 | -------------------------------------------------------------------------------- /lib/accumulate_distribute/events/life_stop.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = async (instance = {}) => { 4 | const { state = {} } = instance 5 | const { timeout } = state 6 | 7 | if (timeout) { 8 | clearTimeout(timeout) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/accumulate_distribute/events/orders_order_cancel.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = async (instance = {}, order) => { 4 | const { state = {}, h = {} } = instance 5 | const { args = {}, orders = {}, gid, timeout } = state 6 | const { emit, debug } = h 7 | const { cancelDelay } = args 8 | 9 | debug('detected atomic cancelation, stopping...') 10 | 11 | if (timeout) { 12 | clearTimeout(timeout) 13 | } 14 | 15 | await emit('exec:order:cancel:all', gid, orders, cancelDelay) 16 | await emit('exec:stop') 17 | } 18 | -------------------------------------------------------------------------------- /lib/accumulate_distribute/events/orders_order_fill.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { Config } = require('bfx-api-node-core') 4 | const { DUST } = Config 5 | 6 | const scheduleTick = require('../util/schedule_tick') 7 | 8 | module.exports = async (instance = {}, order) => { 9 | const { state = {}, h = {} } = instance 10 | const { args = {}, ordersBehind, timeout, currentOrder } = state 11 | const { emit, updateState, debug } = h 12 | const { catchUp } = args 13 | 14 | const newOrdersBehind = Math.max(0, ordersBehind - 1) 15 | const fillAmount = order.getLastFillAmount() 16 | const remainingAmount = state.remainingAmount - fillAmount 17 | const absRem = Math.abs(remainingAmount) 18 | 19 | order.resetFilledAmount() 20 | 21 | debug('updated remaining amount: %f [filled %f]', remainingAmount, fillAmount) 22 | 23 | await updateState(instance, { 24 | remainingAmount, 25 | ordersBehind: newOrdersBehind, 26 | currentOrder: currentOrder + 1 27 | }) 28 | 29 | if (absRem <= DUST) { // stop if finished 30 | if (absRem < 0) { 31 | debug('warning: overfill! %f', absRem) 32 | } 33 | 34 | clearTimeout(timeout) 35 | 36 | return emit('exec:stop') 37 | } 38 | 39 | if (catchUp && newOrdersBehind > 0) { 40 | debug('catching up (behind with %d orders)', newOrdersBehind) 41 | 42 | clearTimeout(timeout) 43 | await scheduleTick(instance, true) // re-schedule early submit 44 | } else if (ordersBehind > 0 && newOrdersBehind === 0) { 45 | debug('caught up!') 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/accumulate_distribute/events/self_interval_tick.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const hasOpenOrders = require('../../util/has_open_orders') 4 | const scheduleTick = require('../util/schedule_tick') 5 | 6 | module.exports = async (instance = {}) => { 7 | const { state = {}, h = {} } = instance 8 | const { orders = {}, args = {}, gid, ordersBehind, orderAmounts, currentOrder } = state 9 | const { emit, emitSelf, debug, updateState } = h 10 | const { 11 | awaitFill, cancelDelay 12 | } = args 13 | 14 | await scheduleTick(instance) 15 | 16 | if (hasOpenOrders(orders)) { // prev order still open 17 | const nextOrdersBehind = Math.min(orderAmounts.length - currentOrder, ordersBehind + 1) 18 | await updateState(instance, { // for catching up 19 | ordersBehind: nextOrdersBehind 20 | }) 21 | 22 | debug('now behind with %d orders', nextOrdersBehind) 23 | 24 | if (!awaitFill) { // cancel current order if not awaiting fill 25 | await emit('exec:order:cancel:all', gid, orders, cancelDelay) 26 | } else { 27 | debug('awaiting fill...') 28 | return // await order fill, then rely on ordersBehind 29 | } 30 | } 31 | 32 | await emitSelf('submit_order') // submit next slice order 33 | } 34 | -------------------------------------------------------------------------------- /lib/accumulate_distribute/events/self_submit_order.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const generateOrder = require('../util/generate_order') 4 | 5 | module.exports = async (instance = {}) => { 6 | const { state = {}, h = {} } = instance 7 | const { emit, debug } = h 8 | const { args = {}, gid } = state 9 | const { submitDelay } = args 10 | 11 | const order = generateOrder(instance) 12 | 13 | if (order) { 14 | debug('generated order for %f @ %f', order.amount, order.price) 15 | await emit('exec:order:submit:all', gid, [order], submitDelay) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/accumulate_distribute/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const defineAlgoOrder = require('../define_algo_order') 4 | 5 | const validateParams = require('./meta/validate_params') 6 | const processParams = require('./meta/process_params') 7 | const initState = require('./meta/init_state') 8 | const onSelfIntervalTick = require('./events/self_interval_tick') 9 | const onSelfSubmitOrder = require('./events/self_submit_order') 10 | const onLifeStart = require('./events/life_start') 11 | const onLifeStop = require('./events/life_stop') 12 | const onOrdersOrderFill = require('./events/orders_order_fill') 13 | const onOrdersOrderCancel = require('./events/orders_order_cancel') 14 | const onDataManagedBook = require('./events/data_managed_book') 15 | const onDataManagedCandles = require('./events/data_managed_candles') 16 | const onDataTrades = require('./events/data_trades') 17 | const onMinimumSizeError = require('./events/error_minimum_size') 18 | const onInsufficientBalanceError = require('./events/insufficient_balance') 19 | const genOrderLabel = require('./meta/gen_order_label') 20 | const genPreview = require('./meta/gen_preview') 21 | const declareEvents = require('./meta/declare_events') 22 | const declareChannels = require('./meta/declare_channels') 23 | const getUIDef = require('./meta/get_ui_def') 24 | const serialize = require('./meta/serialize') 25 | const unserialize = require('./meta/unserialize') 26 | 27 | /** 28 | * Accumulate/Distribute allows you to break up a large order into smaller 29 | * randomized chunks, submitted at regular or irregular intervals to minimise 30 | * detection by other players in the market. 31 | * 32 | * By enabling the 'Await Fill' option, the algorithm will ensure each 33 | * component fills before submitting subsequent orders. Enabling the 'Catch Up' 34 | * flag will cause the algorithm to ignore the slice interval for the next order 35 | * if previous orders have taken longer than expected to fill, thereby ensuring 36 | * the time-to-fill for the entire order is not adversely affected. 37 | * 38 | * The price must be manually specified as `limitPrice` for `LIMIT` order types, 39 | * or as a combination of a price offset & cap for `RELATIVE` order types. 40 | * `MARKET` A/D orders execute using `MARKET` atomic orders, and offer no price 41 | * control. 42 | * 43 | * For `RELATIVE` A/D orders, the price offset & cap can both be set to one of 44 | * the following: 45 | * * Top ask 46 | * * Top bid 47 | * * Orderbook mid price 48 | * * Last trade price 49 | * * Moving Average (configurable period, time frame, candle price) 50 | * * Exponential Moving Average (configurable period, time frame, candle price) 51 | * 52 | * The period limit for moving average targets/caps is `240`, being the number 53 | * of candles returned by the Bitfinex API when subscribing to a candle data 54 | * channel. 55 | * 56 | * @name Accumulate/Distribute 57 | * @param {string} symbol - symbol to trade on 58 | * @param {number} amount - total order amount 59 | * @param {number} sliceAmount - individual slice order amount 60 | * @param {number} sliceInterval - delay in ms between slice orders 61 | * @param {number?} intervalDistortion - slice interval distortion in %, default 0 62 | * @param {number?} amountDistortion - slice amount distortion in %, default 0 63 | * @param {string} orderType - LIMIT, MARKET, RELATIVE 64 | * @param {number?} limitPrice - price for LIMIT orders 65 | * @param {boolean} catchUp - if true, interval will be ignored if behind with filling slices 66 | * @param {boolean} awaitFill - if true, slice orders will be kept open until filled 67 | * @param {Object?} relativeOffset - price reference for RELATIVE orders 68 | * @param {string?} relativeOffset.type - ask, bid, mid, last, ma, or ema 69 | * @param {number?} relativeOffset.delta - offset distance from price reference 70 | * @param {number[]?} relativeOffset.args - MA or EMA indicator arguments [period] 71 | * @param {string?} relativeOffset.candlePrice - 'open', 'high', 'low', 'close' for MA or EMA indicators 72 | * @param {string?} relativeOffset.candleTimeFrame - '1m', '5m', '1D', etc, for MA or EMA indicators 73 | * @param {Object?} relativeCap - maximum price reference for RELATIVE orders 74 | * @param {string?} relativeCap.type - ask, bid, mid, last, ma, or ema 75 | * @param {number?} relativeCap.delta - cap distance from price reference 76 | * @param {number[]?} relativeCap.args - MA or EMA indicator arguments [period] 77 | * @param {string?} relativeCap.candlePrice - 'open', 'high', 'low', 'close' for MA or EMA indicators 78 | * @param {string?} relativeCap.candleTimeFrame - '1m', '5m', '1D', etc, for MA or EMA indicators 79 | * @param {boolean} _margin - if false, order type is prefixed with EXCHANGE 80 | */ 81 | const AccumulateDistribute = defineAlgoOrder({ 82 | id: 'bfx.accumulate_distribute', 83 | name: 'Accumulate/Distribute', 84 | 85 | meta: { 86 | validateParams, 87 | processParams, 88 | declareEvents, 89 | declareChannels, 90 | getUIDef, 91 | genOrderLabel, 92 | genPreview, 93 | initState, 94 | serialize, 95 | unserialize 96 | }, 97 | 98 | events: { 99 | self: { 100 | interval_tick: onSelfIntervalTick, 101 | submit_order: onSelfSubmitOrder 102 | }, 103 | 104 | life: { 105 | start: onLifeStart, 106 | stop: onLifeStop 107 | }, 108 | 109 | orders: { 110 | order_fill: onOrdersOrderFill, 111 | order_cancel: onOrdersOrderCancel 112 | }, 113 | 114 | data: { 115 | managedCandles: onDataManagedCandles, 116 | managedBook: onDataManagedBook, 117 | trades: onDataTrades 118 | }, 119 | 120 | errors: { 121 | minimum_size: onMinimumSizeError, 122 | insufficient_balance: onInsufficientBalanceError 123 | } 124 | } 125 | }) 126 | 127 | module.exports = AccumulateDistribute 128 | -------------------------------------------------------------------------------- /lib/accumulate_distribute/meta/declare_channels.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const hasOBRequirement = require('../util/has_ob_requirement') 4 | const hasTradeRequirement = require('../util/has_trade_requirement') 5 | const hasIndicatorCap = require('../util/has_indicator_cap') 6 | const hasIndicatorOffset = require('../util/has_indicator_offset') 7 | 8 | module.exports = async (instance = {}, host) => { 9 | const { h = {}, state = {} } = instance 10 | const { args = {} } = state 11 | const { symbol } = args 12 | const { declareChannel } = h 13 | 14 | if (hasTradeRequirement(args)) { 15 | await declareChannel(instance, host, 'trades', { symbol }) 16 | } 17 | 18 | if (hasOBRequirement(args)) { 19 | await declareChannel(instance, host, 'book', { 20 | symbol, 21 | prec: 'R0', 22 | len: '25' 23 | }) 24 | } 25 | 26 | const candleChannels = [] 27 | 28 | if (hasIndicatorCap(args)) { 29 | const { candleTimeFrame } = args.relativeCap 30 | candleChannels.push(candleTimeFrame) 31 | } 32 | 33 | if (hasIndicatorOffset(args)) { 34 | const { candleTimeFrame } = args.relativeOffset 35 | 36 | if (candleTimeFrame !== candleChannels[0]) { // different channel from cap 37 | candleChannels.push(candleTimeFrame) 38 | } 39 | } 40 | 41 | for (let i = 0; i < candleChannels.length; i += 1) { // cap/offset candles 42 | await declareChannel(instance, host, 'candles', { 43 | key: `trade:${candleChannels[i]}:${symbol}` 44 | }) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/accumulate_distribute/meta/declare_events.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = (instance = {}, host) => { 4 | const { h = {} } = instance 5 | const { declareEvent } = h 6 | 7 | declareEvent(instance, host, 'self:submit_order', 'submit_order') 8 | declareEvent(instance, host, 'self:interval_tick', 'interval_tick') 9 | } 10 | -------------------------------------------------------------------------------- /lib/accumulate_distribute/meta/gen_order_label.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = (state = {}) => { 4 | const { args = {} } = state 5 | const { orderType, amount, limitPrice, sliceAmount, sliceInterval } = args 6 | 7 | const labelParts = [ 8 | 'A/D', 9 | ` | ${amount} @ ${limitPrice || orderType} `, 10 | ` | slice ${sliceAmount}`, 11 | ' | interval ', Math.floor(sliceInterval / 1000), 's' 12 | ] 13 | 14 | if (orderType === 'LIMIT') { 15 | labelParts.push(` | LIMIT ${args.limitPrice}`) 16 | } else if (orderType === 'MARKET') { 17 | labelParts.push(' | MARKET') 18 | } else { 19 | labelParts.push(` | Offset ${args.relativeOffset.type.toUpperCase()}`) 20 | labelParts.push(` | Cap ${args.relativeCap.type.toUpperCase()}`) 21 | } 22 | 23 | return labelParts.join('') 24 | } 25 | -------------------------------------------------------------------------------- /lib/accumulate_distribute/meta/gen_preview.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { nonce } = require('bfx-api-node-util') 4 | const { Order } = require('bfx-api-node-models') 5 | const genOrderAmounts = require('../util/gen_order_amounts') 6 | 7 | module.exports = (args = {}) => { 8 | const { 9 | symbol, sliceInterval, intervalDistortion, _margin, orderType, limitPrice, 10 | hidden 11 | } = args 12 | 13 | const orderAmounts = genOrderAmounts(args) 14 | const orders = [] 15 | 16 | orderAmounts.map((amount, i) => { 17 | if (orderType === 'MARKET') { 18 | orders.push(new Order({ 19 | symbol, 20 | amount, 21 | hidden, 22 | cid: nonce(), 23 | type: _margin ? 'MARKET' : 'EXCHANGE MARKET' 24 | })) 25 | } else if (orderType === 'LIMIT') { 26 | orders.push(new Order({ 27 | symbol, 28 | amount, 29 | hidden, 30 | price: limitPrice, 31 | cid: nonce(), 32 | type: _margin ? 'LIMIT' : 'EXCHANGE LIMIT' 33 | })) 34 | } else if (orderType === 'RELATIVE') { 35 | orders.push(new Order({ 36 | symbol, 37 | amount, 38 | hidden, 39 | price: 'RELATIVE', 40 | cid: nonce(), 41 | type: _margin ? 'LIMIT' : 'EXCHANGE LIMIT' 42 | })) 43 | } else { 44 | throw new Error(`unknown order type: ${orderType}`) 45 | } 46 | 47 | const m = Math.random() > 0.5 ? 1 : -1 48 | const interval = intervalDistortion === 0 49 | ? sliceInterval 50 | : sliceInterval * (1 + (Math.random() * intervalDistortion * m)) 51 | 52 | if (i !== orderAmounts.length - 1) { 53 | orders.push({ 54 | label: `DELAY ${Math.floor(interval / 1000)}s` 55 | }) 56 | } 57 | }) 58 | 59 | return orders 60 | } 61 | -------------------------------------------------------------------------------- /lib/accumulate_distribute/meta/get_ui_def.js: -------------------------------------------------------------------------------- 1 | module.exports = () => ({ 2 | label: 'Accumulate/Distribute', 3 | customHelp: 'Accumulate/Distribute allows you to break up a large order into smaller randomized chunks, submitted at regular or irregular intervals to minimise detection by other players in the market.\n\nBy enabling the \'Await Fill\' option, the algorithm will ensure each component fills before submitting subsequent orders.\n\nEnabling the \'Catch Up\' flag will cause the algorithm to ignore the slice interval for the next order if previous orders have taken longer than expected to fill, thereby ensuring the time-to-fill for the entire order is not adversely affected.', 4 | connectionTimeout: 10000, 5 | actionTimeout: 10000, 6 | 7 | header: { 8 | component: 'ui.checkbox_group', 9 | fields: ['catchUp', 'awaitFill', 'hidden'] 10 | }, 11 | 12 | sections: [{ 13 | title: '', 14 | name: 'general', 15 | rows: [ 16 | ['orderType', 'amount'], 17 | ['sliceAmount', 'amountDistortion'], 18 | ['sliceIntervalSec', 'intervalDistortion'], 19 | [null, 'limitPrice'] 20 | ] 21 | }, { 22 | title: 'Price Offset', 23 | name: 'offset', 24 | fixed: true, 25 | visible: { 26 | orderType: { eq: 'RELATIVE' } 27 | }, 28 | 29 | rows: [ 30 | ['offsetType', 'offsetDelta'] 31 | ] 32 | }, { 33 | name: 'offsetIndicatorMA', 34 | title: 'Price Offset MA', 35 | fixed: true, 36 | visible: { 37 | orderType: { eq: 'RELATIVE' }, 38 | offsetType: { eq: 'MA' } 39 | }, 40 | rows: [ 41 | ['offsetIndicatorPeriodMA', 'offsetIndicatorPriceMA'], 42 | [null, 'offsetIndicatorTFMA'] 43 | ] 44 | }, { 45 | name: 'offsetIndicatorEMA', 46 | title: 'Price Offset EMA', 47 | fixed: true, 48 | visible: { 49 | orderType: { eq: 'RELATIVE' }, 50 | offsetType: { eq: 'EMA' } 51 | }, 52 | rows: [ 53 | ['offsetIndicatorPeriodEMA', 'offsetIndicatorPriceEMA'], 54 | [null, 'offsetIndicatorTFEMA'] 55 | ] 56 | }, { 57 | title: 'Price Cap', 58 | name: 'cap', 59 | fixed: true, 60 | visible: { 61 | orderType: { eq: 'RELATIVE' } 62 | }, 63 | 64 | rows: [ 65 | ['capType', 'capDelta'] 66 | ] 67 | }, { 68 | title: 'Price Cap MA', 69 | name: 'capIndicatorMA', 70 | fixed: true, 71 | visible: { 72 | orderType: { eq: 'RELATIVE' }, 73 | capType: { eq: 'MA' } 74 | }, 75 | rows: [ 76 | ['capIndicatorPeriodMA', 'capIndicatorPriceMA'], 77 | [null, 'capIndicatorTFMA'] 78 | ] 79 | }, { 80 | title: 'Price Cap EMA', 81 | name: 'capIndicatorEMA', 82 | fixed: true, 83 | visible: { 84 | orderType: { eq: 'RELATIVE' }, 85 | capType: { eq: 'EMA' } 86 | }, 87 | rows: [ 88 | ['capIndicatorPeriodEMA', 'capIndicatorPriceEMA'], 89 | [null, 'capIndicatorTFEMA'] 90 | ] 91 | }, { 92 | name: 'actions', 93 | rows: [ 94 | ['submitDelay', 'cancelDelay'], 95 | ['action', null] 96 | ] 97 | }], 98 | 99 | fields: { 100 | // General section/header 101 | catchUp: { 102 | component: 'input.checkbox', 103 | label: 'Catch Up', 104 | default: true, 105 | customHelp: 'If the algo falls behind in filling orders, disregard the slice interval and submit the next order after a 2 second delay' 106 | }, 107 | 108 | awaitFill: { 109 | component: 'input.checkbox', 110 | label: 'Await Fill', 111 | default: true, 112 | customHelp: 'Keeps the current order open until it fills, while tracking progress for \'Catch up\'' 113 | }, 114 | 115 | hidden: { 116 | component: 'input.checkbox', 117 | label: 'HIDDEN', 118 | default: false, 119 | help: 'trading.hideorder_tooltip' 120 | }, 121 | 122 | orderType: { 123 | component: 'input.dropdown', 124 | label: 'Order Type', 125 | default: 'MARKET', 126 | options: { 127 | LIMIT: 'Limit', 128 | MARKET: 'Market', 129 | RELATIVE: 'Relative' 130 | } 131 | }, 132 | 133 | amount: { 134 | component: 'input.amount', 135 | label: 'Amount $BASE', 136 | customHelp: 'Total order amount, to be executed slice-by-slice', 137 | priceField: 'limitPrice' 138 | }, 139 | 140 | sliceAmount: { 141 | component: 'input.number', 142 | label: 'Slice Amount $BASE', 143 | customHelp: 'Allows individual buy & sell amounts to be adjusted' 144 | }, 145 | 146 | amountDistortion: { 147 | component: 'input.percent', 148 | label: 'Amount Distortion %', 149 | customHelp: 'Amount to distort individual order sizes to prevent detection, in percent' 150 | }, 151 | 152 | sliceIntervalSec: { 153 | component: 'input.number', 154 | label: 'Slice Interval S', 155 | customHelp: 'Time to wait between each slice order, in seconds' 156 | }, 157 | 158 | intervalDistortion: { 159 | component: 'input.percent', 160 | label: 'Interval Distortion %', 161 | customHelp: 'Amount to distort each slice interval, in percent' 162 | }, 163 | 164 | limitPrice: { 165 | component: 'input.price', 166 | label: 'Price $QUOTE', 167 | customHelp: 'Price for LIMIT order type', 168 | visible: { 169 | orderType: { eq: 'LIMIT' } 170 | } 171 | }, 172 | 173 | // Offset section 174 | offsetType: { 175 | component: 'input.dropdown', 176 | label: 'Offset Type', 177 | default: 'MID', 178 | customHelp: 'Relative order price as offset from an indicator/book/last trade', 179 | options: { 180 | BID: 'Top Bid', 181 | ASK: 'Top Ask', 182 | MID: 'Book Mid Price', 183 | TRADE: 'Last Trade Price', 184 | MA: 'Moving Average', 185 | EMA: 'Exp Moving Average' 186 | } 187 | }, 188 | 189 | offsetDelta: { 190 | component: 'input.number', 191 | label: 'Offset Delta', 192 | customHelp: 'Price as distance from offset value', 193 | default: 0 194 | }, 195 | 196 | offsetIndicatorPeriodMA: { 197 | component: 'input.number', 198 | label: 'MA Period', 199 | customHelp: 'Period for moving average indicator', 200 | visible: { 201 | offsetType: { eq: 'MA' } 202 | } 203 | }, 204 | 205 | offsetIndicatorPriceMA: { 206 | component: 'input.dropdown', 207 | label: 'MA Candle Price', 208 | default: 'CLOSE', 209 | options: { 210 | OPEN: 'Open', 211 | HIGH: 'High', 212 | LOW: 'Low', 213 | CLOSE: 'Close' 214 | }, 215 | 216 | visible: { 217 | offsetType: { eq: 'MA' } 218 | } 219 | }, 220 | 221 | offsetIndicatorPeriodEMA: { 222 | component: 'input.number', 223 | label: 'EMA Period', 224 | customHelp: 'Period for exponential moving average indicator', 225 | visible: { 226 | offsetType: { eq: 'EMA' } 227 | } 228 | }, 229 | 230 | offsetIndicatorPriceEMA: { 231 | component: 'input.dropdown', 232 | label: 'EMA Candle Price', 233 | default: 'CLOSE', 234 | options: { 235 | OPEN: 'Open', 236 | HIGH: 'High', 237 | LOW: 'Low', 238 | CLOSE: 'Close' 239 | }, 240 | 241 | visible: { 242 | offsetType: { eq: 'EMA' } 243 | } 244 | }, 245 | 246 | offsetIndicatorTFMA: { 247 | component: 'input.dropdown', 248 | label: 'MA Candle Time Frame', 249 | default: 'ONE_HOUR', 250 | options: { 251 | ONE_MINUTE: '1m', 252 | FIVE_MINUTES: '5m', 253 | FIFTEEN_MINUTES: '15m', 254 | THIRTY_MINUTES: '30m', 255 | ONE_HOUR: '1h', 256 | THREE_HOURS: '3h', 257 | SIX_HOURS: '6h', 258 | TWELVE_HOURS: '12h', 259 | ONE_DAY: '1D', 260 | SEVEN_DAYS: '7D', 261 | FOURTEEN_DAYS: '14D', 262 | ONE_MONTH: '1M' 263 | }, 264 | 265 | visible: { 266 | offsetType: { eq: 'MA' } 267 | } 268 | }, 269 | 270 | offsetIndicatorTFEMA: { 271 | component: 'input.dropdown', 272 | label: 'EMA Candle Time Frame', 273 | default: 'ONE_HOUR', 274 | options: { 275 | ONE_MINUTE: '1m', 276 | FIVE_MINUTES: '5m', 277 | FIFTEEN_MINUTES: '15m', 278 | THIRTY_MINUTES: '30m', 279 | ONE_HOUR: '1h', 280 | THREE_HOURS: '3h', 281 | SIX_HOURS: '6h', 282 | TWELVE_HOURS: '12h', 283 | ONE_DAY: '1D', 284 | SEVEN_DAYS: '7D', 285 | FOURTEEN_DAYS: '14D', 286 | ONE_MONTH: '1M' 287 | }, 288 | 289 | visible: { 290 | offsetType: { eq: 'EMA' } 291 | } 292 | }, 293 | 294 | // Cap section 295 | capType: { 296 | component: 'input.dropdown', 297 | label: 'Price Cap Type', 298 | default: 'MID', 299 | customHelp: 'Upper price limit for relative order type', 300 | options: { 301 | BID: 'Top Bid', 302 | ASK: 'Top Ask', 303 | MID: 'Book Mid Price', 304 | TRADE: 'Last Trade Price', 305 | MA: 'Moving Average', 306 | EMA: 'Exp Moving Average', 307 | NONE: 'None' 308 | } 309 | }, 310 | 311 | capDelta: { 312 | component: 'input.number', 313 | label: 'Cap Delta', 314 | customHelp: 'Price as distance from cap value', 315 | default: 0, 316 | 317 | disabled: { 318 | capType: { eq: 'NONE' } 319 | } 320 | }, 321 | 322 | capIndicatorPeriodMA: { 323 | component: 'input.number', 324 | label: 'MA Period', 325 | customHelp: 'Period for moving average indicator', 326 | visible: { 327 | capType: { eq: 'MA' } 328 | } 329 | }, 330 | 331 | capIndicatorPriceMA: { 332 | component: 'input.dropdown', 333 | label: 'MA Candle Price', 334 | default: 'CLOSE', 335 | options: { 336 | OPEN: 'Open', 337 | HIGH: 'High', 338 | LOW: 'Low', 339 | CLOSE: 'Close' 340 | }, 341 | visible: { 342 | capType: { eq: 'MA' } 343 | } 344 | }, 345 | 346 | capIndicatorPeriodEMA: { 347 | component: 'input.number', 348 | label: 'EMA Period', 349 | customHelp: 'Period for exponential moving average indicator', 350 | visible: { 351 | capType: { eq: 'EMA' } 352 | } 353 | }, 354 | 355 | capIndicatorPriceEMA: { 356 | component: 'input.dropdown', 357 | label: 'EMA Candle Price', 358 | default: 'CLOSE', 359 | options: { 360 | OPEN: 'Open', 361 | HIGH: 'High', 362 | LOW: 'Low', 363 | CLOSE: 'Close' 364 | }, 365 | visible: { 366 | capType: { eq: 'EMA' } 367 | } 368 | }, 369 | 370 | capIndicatorTFMA: { 371 | component: 'input.dropdown', 372 | label: 'MA Candle Time Frame', 373 | default: 'ONE_HOUR', 374 | options: { 375 | ONE_MINUTE: '1m', 376 | FIVE_MINUTES: '5m', 377 | FIFTEEN_MINUTES: '15m', 378 | THIRTY_MINUTES: '30m', 379 | ONE_HOUR: '1h', 380 | THREE_HOURS: '3h', 381 | SIX_HOURS: '6h', 382 | TWELVE_HOURS: '12h', 383 | ONE_DAY: '1D', 384 | SEVEN_DAYS: '7D', 385 | FOURTEEN_DAYS: '14D', 386 | ONE_MONTH: '1M' 387 | }, 388 | visible: { 389 | capType: { eq: 'MA' } 390 | } 391 | }, 392 | 393 | capIndicatorTFEMA: { 394 | component: 'input.dropdown', 395 | label: 'EMA Candle Time Frame', 396 | default: 'ONE_HOUR', 397 | options: { 398 | ONE_MINUTE: '1m', 399 | FIVE_MINUTES: '5m', 400 | FIFTEEN_MINUTES: '15m', 401 | THIRTY_MINUTES: '30m', 402 | ONE_HOUR: '1h', 403 | THREE_HOURS: '3h', 404 | SIX_HOURS: '6h', 405 | TWELVE_HOURS: '12h', 406 | ONE_DAY: '1D', 407 | SEVEN_DAYS: '7D', 408 | FOURTEEN_DAYS: '14D', 409 | ONE_MONTH: '1M' 410 | }, 411 | visible: { 412 | capType: { eq: 'EMA' } 413 | } 414 | }, 415 | 416 | // Action section 417 | submitDelay: { 418 | component: 'input.number', 419 | label: 'Submit Delay (sec)', 420 | customHelp: 'Seconds to wait before submitting orders', 421 | default: 2 422 | }, 423 | 424 | cancelDelay: { 425 | component: 'input.number', 426 | label: 'Cancel Delay (sec)', 427 | customHelp: 'Seconds to wait before cancelling orders', 428 | default: 1 429 | }, 430 | 431 | action: { 432 | component: 'input.radio', 433 | label: 'Action', 434 | options: ['Buy', 'Sell'], 435 | inline: true, 436 | default: 'Buy' 437 | } 438 | }, 439 | 440 | actions: ['preview', 'submit'] 441 | }) 442 | -------------------------------------------------------------------------------- /lib/accumulate_distribute/meta/init_state.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { Config } = require('bfx-api-node-core') 4 | const { DUST } = Config 5 | 6 | const genOrderAmounts = require('../util/gen_order_amounts') 7 | 8 | module.exports = (args = {}) => { 9 | const { amount } = args 10 | const orderAmounts = genOrderAmounts(args) 11 | 12 | // TODO: Remove sanity check 13 | let totalAmount = 0 14 | orderAmounts.forEach(a => { totalAmount += a }) 15 | 16 | if (Math.abs(totalAmount - amount) > DUST) { 17 | throw new Error(`total order amount is too large: ${totalAmount} > ${amount}`) 18 | } 19 | 20 | return { 21 | args, 22 | orderAmounts, 23 | currentOrder: 0, 24 | ordersBehind: 0, 25 | remainingAmount: amount 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/accumulate_distribute/meta/process_params.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { TIME_FRAMES } = require('bfx-hf-util') 4 | 5 | module.exports = (data) => { 6 | const params = { ...data } 7 | 8 | if (params._symbol) { 9 | params.symbol = params._symbol 10 | delete params._symbol 11 | } 12 | 13 | if (!params.cancelDelay) { 14 | params.cancelDelay = 1500 15 | } 16 | 17 | if (!params.submitDelay) { 18 | params.submitDelay = 5000 19 | } 20 | 21 | if (params.sliceIntervalSec) { 22 | params.sliceInterval = (+params.sliceIntervalSec) * 1000 23 | delete params.sliceIntervalSec 24 | } 25 | 26 | if (!params.amountDistortion) { 27 | params.amountDistortion = 0 28 | } 29 | 30 | if (!params.intervalDistortion) { 31 | params.intervalDistortion = 0 32 | } 33 | 34 | if (params.orderType === 'RELATIVE') { 35 | params.relativeOffset = { 36 | type: params.offsetType.toLowerCase(), 37 | delta: +params.offsetDelta 38 | } 39 | 40 | params.relativeCap = { 41 | type: params.capType.toLowerCase(), 42 | delta: +params.capDelta 43 | } 44 | 45 | if (params.offsetType === 'MA') { 46 | params.relativeOffset.candlePrice = params.offsetIndicatorPriceMA.toLowerCase() 47 | params.relativeOffset.candleTimeFrame = TIME_FRAMES[params.offsetIndicatorTFMA] 48 | params.relativeOffset.args = [+params.offsetIndicatorPeriodMA] 49 | } else if (params.offsetType === 'EMA') { 50 | params.relativeOffset.candlePrice = params.offsetIndicatorPriceEMA.toLowerCase() 51 | params.relativeOffset.candleTimeFrame = TIME_FRAMES[params.offsetIndicatorTFEMA] 52 | params.relativeOffset.args = [+params.offsetIndicatorPeriodEMA] 53 | } 54 | 55 | if (params.capType === 'MA') { 56 | params.relativeCap.candlePrice = params.capIndicatorPriceMA.toLowerCase() 57 | params.relativeCap.candleTimeFrame = TIME_FRAMES[params.capIndicatorTFMA] 58 | params.relativeCap.args = [+params.capIndicatorPeriodMA] 59 | } else if (params.capType === 'EMA') { 60 | params.relativeCap.candlePrice = params.capIndicatorPriceEMA.toLowerCase() 61 | params.relativeCap.candleTimeFrame = TIME_FRAMES[params.capIndicatorTFEMA] 62 | params.relativeCap.args = [+params.capIndicatorPeriodEMA] 63 | } 64 | } 65 | 66 | if (params.action) { 67 | if (params.action === 'Sell') { 68 | params.amount = (+params.amount) * -1 69 | params.sliceAmount = (+params.sliceAmount) * -1 70 | } 71 | 72 | delete params.action 73 | } 74 | 75 | return params 76 | } 77 | -------------------------------------------------------------------------------- /lib/accumulate_distribute/meta/serialize.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = (state = {}) => { 4 | const { 5 | remainingAmount, orderAmounts, currentOrder, ordersBehind, args = {} 6 | } = state 7 | 8 | return { 9 | remainingAmount, 10 | orderAmounts, 11 | currentOrder, 12 | ordersBehind, 13 | args 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/accumulate_distribute/meta/unserialize.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = (loadedState = {}) => { 4 | const { 5 | remainingAmount, orderAmounts, currentOrder, ordersBehind, args = {} 6 | } = loadedState 7 | 8 | return { 9 | remainingAmount, 10 | orderAmounts, 11 | currentOrder, 12 | ordersBehind, 13 | args 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/accumulate_distribute/meta/validate_params.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { TIME_FRAME_WIDTHS } = require('bfx-hf-util') 4 | const _isFinite = require('lodash/isFinite') 5 | const _isObject = require('lodash/isObject') 6 | const _isBoolean = require('lodash/isBoolean') 7 | 8 | const ORDER_TYPES = ['MARKET', 'LIMIT', 'RELATIVE'] 9 | 10 | module.exports = (args = {}) => { 11 | const { 12 | limitPrice, amount, sliceAmount, orderType, submitDelay, cancelDelay, 13 | intervalDistortion, amountDistortion, sliceInterval, relativeOffset, 14 | relativeCap, catchUp, awaitFill 15 | } = args 16 | 17 | if (ORDER_TYPES.indexOf(orderType) === -1) return `Invalid order type: ${orderType}` 18 | if (!_isFinite(amount)) return 'Invalid amount' 19 | if (!_isFinite(sliceAmount)) return 'Invalid slice amount' 20 | if (!_isFinite(submitDelay) || submitDelay < 0) return 'Invalid submit delay' 21 | if (!_isFinite(cancelDelay) || cancelDelay < 0) return 'Invalid cancel delay' 22 | if (!_isBoolean(catchUp)) return 'Bool catch up flag required' 23 | if (!_isBoolean(awaitFill)) return 'Bool await fill flag required' 24 | if (!_isFinite(sliceInterval) || sliceInterval <= 0) return 'Invalid slice interval' 25 | if (!_isFinite(intervalDistortion)) return 'Interval distortion required' 26 | if (!_isFinite(amountDistortion)) return 'Amount distortion required' 27 | if (orderType === 'LIMIT' && !_isFinite(limitPrice)) { 28 | return 'Limit price required for LIMIT order type' 29 | } 30 | 31 | if (_isObject(relativeCap)) { 32 | if (!_isFinite(relativeCap.delta)) { 33 | return 'Invalid relative cap delta' 34 | } 35 | 36 | if ((relativeCap.type === 'ma') || (relativeCap.type === 'ema')) { 37 | const { args = [] } = relativeCap 38 | 39 | if (args.length !== 1) { 40 | return 'Invalid args for relative cap indicator' 41 | } 42 | 43 | if (!relativeCap.candlePrice) { 44 | return 'Candle price required for relative cap indicator' 45 | } else if (!relativeCap.candleTimeFrame) { 46 | return 'Candle time frame required for relative cap indicator' 47 | } else if (!TIME_FRAME_WIDTHS[relativeCap.candleTimeFrame]) { 48 | return `Unrecognized relative cap candle time frame: ${relativeCap.candleTimeFrame}` 49 | } else if (!_isFinite(relativeCap.args[0])) { 50 | return `Invalid relative cap indicator period: ${relativeCap.args[0]}` 51 | } 52 | } 53 | } 54 | 55 | if (_isObject(relativeOffset)) { 56 | if (!_isFinite(relativeOffset.delta)) { 57 | return 'Invalid relative offset delta' 58 | } 59 | 60 | if ((relativeOffset.type === 'ma') || (relativeOffset.type === 'ema')) { 61 | const { args = [] } = relativeOffset 62 | 63 | if (args.length !== 1) { 64 | return 'Invalid args for relative offset indicator' 65 | } 66 | 67 | if (!relativeOffset.candlePrice) { 68 | return 'Candle price required for relative offset indicator' 69 | } else if (!relativeOffset.candleTimeFrame) { 70 | return 'Candle time frame required for relative offset indicator' 71 | } else if (!TIME_FRAME_WIDTHS[relativeOffset.candleTimeFrame]) { 72 | return `Unrecognized relative offset candle time frame: ${relativeOffset.candleTimeFrame}` 73 | } else if (!_isFinite(relativeOffset.args[0])) { 74 | return `Invalid relative offset indicator period: ${relativeOffset.args[0]}` 75 | } 76 | } 77 | } 78 | 79 | if ( 80 | (amount < 0 && sliceAmount >= 0) || 81 | (amount > 0 && sliceAmount <= 0) 82 | ) { 83 | return 'Amount & slice amount must have same sign' 84 | } 85 | 86 | return null 87 | } 88 | -------------------------------------------------------------------------------- /lib/accumulate_distribute/util/gen_order_amounts.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const _isFinite = require('lodash/isFinite') 4 | const { prepareAmount } = require('bfx-api-node-util') 5 | const { Config } = require('bfx-api-node-core') 6 | const { DUST } = Config 7 | 8 | module.exports = (args = {}) => { 9 | const { amount, sliceAmount, amountDistortion } = args 10 | let orderAmounts = [] 11 | 12 | if (_isFinite(amountDistortion)) { 13 | let totalAmount = 0 14 | 15 | while (Math.abs(amount - totalAmount) > DUST) { 16 | const m = Math.random() > 0.5 ? 1 : -1 17 | const orderAmount = sliceAmount * (1 + (Math.random() * amountDistortion * m)) 18 | const remAmount = amount - totalAmount 19 | const cappedOrderAmount = +prepareAmount(remAmount < 0 20 | ? Math.max(remAmount, orderAmount) 21 | : Math.min(remAmount, orderAmount) 22 | ) 23 | 24 | orderAmounts.push(cappedOrderAmount) 25 | totalAmount += cappedOrderAmount 26 | } 27 | } else { 28 | const n = Math.ceil(amount / sliceAmount) 29 | orderAmounts = Array.apply(null, Array(n)).map(() => sliceAmount) 30 | } 31 | 32 | return orderAmounts 33 | } 34 | -------------------------------------------------------------------------------- /lib/accumulate_distribute/util/generate_order.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const _isFinite = require('lodash/isFinite') 4 | const _isObject = require('lodash/isObject') 5 | const { Order } = require('bfx-api-node-models') 6 | const { nonce } = require('bfx-api-node-util') 7 | 8 | module.exports = (instance = {}) => { 9 | const { state = {}, h = {} } = instance 10 | const { debug } = h 11 | const { 12 | args = {}, orderAmounts, currentOrder, lastBook, lastTrade, 13 | capIndicator, offsetIndicator, remainingAmount 14 | } = state 15 | 16 | const { 17 | symbol, orderType, relativeOffset, relativeCap, limitPrice, _margin, hidden 18 | } = args 19 | 20 | const scheduledAmount = orderAmounts[Math.min(currentOrder, orderAmounts.length - 1)] 21 | const amount = scheduledAmount > 0 22 | ? Math.min(scheduledAmount, remainingAmount) 23 | : Math.max(scheduledAmount, remainingAmount) 24 | 25 | if (orderType === 'MARKET') { 26 | return new Order({ 27 | symbol, 28 | amount, 29 | hidden, 30 | cid: nonce(), 31 | type: _margin ? 'MARKET' : 'EXCHANGE MARKET' 32 | }) 33 | } else if (orderType === 'LIMIT') { 34 | return new Order({ 35 | symbol, 36 | amount, 37 | hidden, 38 | price: limitPrice, 39 | cid: nonce(), 40 | type: _margin ? 'LIMIT' : 'EXCHANGE LIMIT' 41 | }) 42 | } else if (orderType !== 'RELATIVE') { 43 | throw new Error(`unknown order type: ${orderType}`) 44 | } 45 | 46 | let offsetPrice 47 | 48 | switch (relativeOffset.type) { 49 | case 'trade': { 50 | offsetPrice = lastTrade.price 51 | break 52 | } 53 | 54 | case 'bid': { 55 | offsetPrice = lastBook.topBid() 56 | break 57 | } 58 | 59 | case 'ask': { 60 | offsetPrice = lastBook.topAsk() 61 | break 62 | } 63 | 64 | case 'mid': { 65 | offsetPrice = lastBook.midPrice() 66 | break 67 | } 68 | 69 | case 'ema': { 70 | offsetPrice = offsetIndicator.v() // guaranteed seeded 71 | break 72 | } 73 | 74 | case 'ma': { 75 | offsetPrice = offsetIndicator.v() // guaranteed seeded 76 | break 77 | } 78 | 79 | default: { 80 | throw new Error(`unknown relative offset type: ${relativeOffset.type}`) 81 | } 82 | } 83 | 84 | if (!_isFinite(offsetPrice)) { 85 | return null // awaiting data 86 | } 87 | 88 | debug('resolved offset price %f', offsetPrice) 89 | 90 | let finalPrice = offsetPrice + relativeOffset.delta 91 | 92 | if (_isObject(relativeCap) && relativeCap.type !== 'none') { 93 | let priceCap 94 | 95 | switch (relativeCap.type) { 96 | case 'trade': { 97 | priceCap = lastTrade.price 98 | break 99 | } 100 | 101 | case 'bid': { 102 | priceCap = lastBook.topBid() 103 | break 104 | } 105 | 106 | case 'ask': { 107 | priceCap = lastBook.topAsk() 108 | break 109 | } 110 | 111 | case 'mid': { 112 | priceCap = lastBook.midPrice() 113 | break 114 | } 115 | 116 | case 'ema': { 117 | priceCap = capIndicator.v() // guaranteed seeded 118 | break 119 | } 120 | 121 | case 'ma': { 122 | priceCap = capIndicator.v() // guaranteed seeded 123 | break 124 | } 125 | 126 | default: { 127 | throw new Error(`unknown relative cap type: ${relativeCap.type}`) 128 | } 129 | } 130 | 131 | if (!_isFinite(priceCap)) { 132 | return null // awaiting data 133 | } 134 | 135 | priceCap += relativeCap.delta 136 | 137 | debug('resolved cap price %f', priceCap) 138 | 139 | finalPrice = Math.min(finalPrice, priceCap) 140 | } 141 | 142 | return new Order({ 143 | symbol, 144 | amount, 145 | hidden, 146 | price: finalPrice, 147 | cid: nonce(), 148 | type: _margin ? 'LIMIT' : 'EXCHANGE LIMIT' 149 | }) 150 | } 151 | -------------------------------------------------------------------------------- /lib/accumulate_distribute/util/has_indicator_cap.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = (args = {}) => { 4 | const { relativeCap = {} } = args 5 | const { type } = relativeCap 6 | 7 | return (type === 'ma' || type === 'ema') 8 | } 9 | -------------------------------------------------------------------------------- /lib/accumulate_distribute/util/has_indicator_offset.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = (args = {}) => { 4 | const { relativeOffset = {} } = args 5 | const { type } = relativeOffset 6 | 7 | return (type === 'ma' || type === 'ema') 8 | } 9 | -------------------------------------------------------------------------------- /lib/accumulate_distribute/util/has_ob_requirement.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = (args = {}) => { 4 | const { relativeOffset = {}, relativeCap = {} } = args 5 | const offsetType = relativeOffset.type 6 | const capType = relativeCap.type 7 | 8 | return ( 9 | (offsetType === 'bid' || offsetType === 'ask' || offsetType === 'mid') || 10 | (capType === 'bid' || capType === 'ask' || capType === 'mid') 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /lib/accumulate_distribute/util/has_trade_requirement.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = (args = {}) => { 4 | const { relativeOffset = {}, relativeCap = {} } = args 5 | const offsetType = relativeOffset.type 6 | const capType = relativeCap.type 7 | 8 | return offsetType === 'trade' || capType === 'trade' 9 | } 10 | -------------------------------------------------------------------------------- /lib/accumulate_distribute/util/schedule_tick.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const _isFinite = require('lodash/isFinite') 4 | 5 | // Note if catching up, the delay is always 2s 6 | module.exports = async (instance, catchUp) => { 7 | const { state = {}, h = {} } = instance 8 | const { emitSelf, updateState, notifyUI } = h 9 | const { args = {} } = state 10 | const { sliceInterval, intervalDistortion } = args 11 | let timeoutDelay = catchUp ? 2000 : sliceInterval 12 | 13 | // Distort timeout interval if requested 14 | if (_isFinite(intervalDistortion) && !catchUp) { 15 | const m = Math.random() > 0.5 ? 1 : -1 16 | timeoutDelay *= 1 + (Math.random() * intervalDistortion * m) 17 | } 18 | 19 | const timeout = setTimeout(async () => { // schedule first tick 20 | await emitSelf('interval_tick') 21 | }, timeoutDelay) 22 | 23 | await notifyUI('info', `scheduled tick in ~${Math.floor(timeoutDelay / 1000)}s`) 24 | await updateState(instance, { 25 | timeout, 26 | timeoutDelay, 27 | timeoutScheduledAt: Date.now() 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /lib/async_event_emitter.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const PI = require('p-iteration') 4 | 5 | /** 6 | * Event emitter class that provides an async `emit` function, useful for when 7 | * one needs to `await` the event and all of its listeners. 8 | */ 9 | module.exports = class AsyncEventEmitter { 10 | constructor () { 11 | this.listeners = {} 12 | } 13 | 14 | /** 15 | * Removes all listeners, or only those for the specified event name 16 | * 17 | * @param {string} eventName 18 | */ 19 | removeAllListeners (eventName) { 20 | if (eventName) { 21 | delete this.listeners[eventName] 22 | } else { 23 | this.listeners = {} 24 | } 25 | } 26 | 27 | /** 28 | * Remove an event handler by event name 29 | * 30 | * @param {string} eventName 31 | * @param {Method} cb 32 | */ 33 | off (eventName, cb) { 34 | if (!this.listeners[eventName]) { 35 | const i = this.listeners[eventName].findIndex(l => ( 36 | l.cb === cb 37 | )) 38 | 39 | if (i !== -1) { 40 | this.listeners[eventName].splice(i, 1) 41 | } 42 | } 43 | } 44 | 45 | /** 46 | * Bind an event handler that should only fire once 47 | * 48 | * @param {string} eventName 49 | * @param {Method} cb 50 | */ 51 | once (eventName, cb) { 52 | if (!this.listeners[eventName]) { 53 | this.listeners[eventName] = [] 54 | } 55 | 56 | this.listeners[eventName].push({ 57 | type: 'once', 58 | cb 59 | }) 60 | } 61 | 62 | /** 63 | * Bind an event handler 64 | * 65 | * @param {string} eventName 66 | * @param {Method} cb 67 | */ 68 | on (eventName, cb) { 69 | if (!this.listeners[eventName]) { 70 | this.listeners[eventName] = [] 71 | } 72 | 73 | this.listeners[eventName].push({ 74 | type: 'on', 75 | cb 76 | }) 77 | } 78 | 79 | /** 80 | * Emit an event; can be await'ed, and will resolve after all handlers have 81 | * been called 82 | * 83 | * @param {string} eventName 84 | * @param {...any} args 85 | * @return {Promise} p 86 | */ 87 | async emit (eventName, ...args) { 88 | const listeners = this.listeners[eventName] 89 | const indexesToRemove = [] 90 | 91 | if (!listeners) { 92 | return 93 | } 94 | 95 | await PI.forEach(listeners, async (l, i) => { 96 | await l.cb(...args) 97 | 98 | if (l.type === 'once') { 99 | indexesToRemove.unshift(i) 100 | } 101 | }) 102 | 103 | indexesToRemove.forEach(rmi => listeners.splice(rmi, 1)) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /lib/define_algo_order.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * Stub, reserved API method 5 | * 6 | * @param {Object} definition 7 | * @return {Object} ao 8 | */ 9 | module.exports = (def = {}) => def 10 | -------------------------------------------------------------------------------- /lib/errors/no_data.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class NoDataError extends Error { 4 | constructor (message, extra) { 5 | super() 6 | 7 | Error.captureStackTrace(this, this.constructor) 8 | 9 | this.name = 'NoDataError' 10 | this.message = message || '' 11 | 12 | if (extra) this.extra = extra 13 | } 14 | } 15 | 16 | module.exports = NoDataError 17 | -------------------------------------------------------------------------------- /lib/host/events/assign_channel.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const withAOUpdate = require('../with_ao_update') 4 | 5 | /** 6 | * Meant to be write-only, channels are assigned before life:start, subscribed 7 | * to on life:start, and unsubscribed from on life:stop 8 | * 9 | * @param {Object} aoHost 10 | * @param {string} gid - AO instance GID to operate on 11 | * @param {string} channel - i.e. 'ticker' 12 | * @param {Object} filter - i.e. { symbol: 'tBTCUSD' } 13 | */ 14 | module.exports = async (aoHost, gid, channel, filter) => { 15 | await withAOUpdate(aoHost, gid, async (instance = {}) => { 16 | const { state = {} } = instance 17 | 18 | return { 19 | ...state, 20 | channels: [ 21 | ...state.channels, 22 | { channel, filter } 23 | ] 24 | } 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /lib/host/events/cancel_all_orders.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const _isObject = require('lodash/isObject') 4 | const withAOUpdate = require('../with_ao_update') 5 | 6 | /** 7 | * Cancels all provided orders with the specified delay, and removes them from 8 | * the AO instance state. 9 | * 10 | * @param {Object} aoHost 11 | * @param {string} gid - AO instance gid 12 | * @param {Object[]|Array[]} orders 13 | * @param {number} delay - cancellation delay 14 | * @return {Object} nextInstanceState 15 | */ 16 | module.exports = async (aoHost, gid, orders, delay) => { 17 | await withAOUpdate(aoHost, gid, async (instance = {}) => { 18 | const { state = {}, h = {} } = instance 19 | const { cancelOrderWithDelay, debug } = h 20 | const allOrders = _isObject(orders) 21 | ? Object.values(orders) 22 | : orders 23 | 24 | // Don't try to cancel market orders 25 | const _orders = allOrders 26 | .filter(o => !/MARKET/.test(o.type) && o.id) 27 | 28 | let nextState = state 29 | 30 | for (let i = 0; i < _orders.length; i += 1) { 31 | const o = _orders[i] 32 | 33 | debug( 34 | 'canceling order %s %f @ %f [cid %s id %s]', 35 | o.type, o.amount, o.price, o.cid, o.id 36 | ) 37 | 38 | nextState = await cancelOrderWithDelay(nextState, delay, o) 39 | } 40 | 41 | return nextState 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /lib/host/events/insufficient_balance.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * Propagates the error to all instances that have the relevant listener 5 | * 6 | * @param {Object} aoHost 7 | * @param {string} gid - AO instance GID to operate on 8 | * @param {Order} order - the order that failed due to insufficient balance 9 | * @param {Notification} notification - which reported the error 10 | */ 11 | module.exports = async (aoHost, gid, order, notification) => { 12 | const instance = aoHost.getAOInstance(gid) 13 | 14 | if (!instance) { 15 | return 16 | } 17 | 18 | aoHost.triggerAOEvent(instance, 'errors', 'insufficient_balance', order, notification) 19 | } 20 | -------------------------------------------------------------------------------- /lib/host/events/minimum_size_error.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * Propagates the error to all instances that have the relevant listener 5 | * 6 | * @param {Object} aoHost 7 | * @param {string} gid - AO instance GID to operate on 8 | * @param {Order} order - the order that is below the minimum size for its sym 9 | * @param {Notification} notification - which reported the error 10 | */ 11 | module.exports = async (aoHost, gid, order, notification) => { 12 | const instance = aoHost.getAOInstance(gid) 13 | 14 | if (!instance) { 15 | return 16 | } 17 | 18 | aoHost.triggerAOEvent(instance, 'errors', 'minimum_size', order, notification) 19 | } 20 | -------------------------------------------------------------------------------- /lib/host/events/notify.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const withAOUpdate = require('../with_ao_update') 4 | 5 | /** 6 | * Broadcasts a ucm notification to be picked up by the UI 7 | * 8 | * @param {Object} aoHost 9 | * @param {string} gid - AO instance gid 10 | * @param {string} level - notification level, i.e. 'info', 'success', etc 11 | * @param {string} message 12 | * @return {Object} nextInstanceState 13 | */ 14 | module.exports = async (aoHost, gid, level, message) => { 15 | await withAOUpdate(aoHost, gid, async (instance = {}) => { 16 | const { state = {} } = instance 17 | const { ws: wsState = {} } = state 18 | const { ws } = wsState 19 | 20 | ws.send(JSON.stringify([0, 'n', null, { 21 | type: 'ucm-notify-ui', 22 | info: { 23 | level, 24 | message 25 | } 26 | }])) 27 | 28 | return null 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /lib/host/events/submit_all_orders.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const _isObject = require('lodash/isObject') 4 | const _isFunction = require('lodash/isFunction') 5 | const withAOUpdate = require('../with_ao_update') 6 | 7 | /** 8 | * Submits all provided orders with the specified delay, and adds them to the 9 | * AO instance state. Attaches meta labels if the algo order supports them. 10 | * 11 | * @param {Object} aoHost 12 | * @param {string} gid - AO instance gid 13 | * @param {Object[]|Array[]} orders 14 | * @param {number} delay - cancellation delay 15 | * @return {Object} nextInstanceState 16 | */ 17 | module.exports = async (aoHost, gid, orders, delay) => { 18 | await withAOUpdate(aoHost, gid, async (instance = {}) => { 19 | const { state = {}, h = {} } = instance 20 | const { id } = state 21 | const { submitOrderWithDelay, debug } = h 22 | const _orders = _isObject(orders) 23 | ? Object.values(orders) 24 | : orders 25 | 26 | const ao = aoHost.getAO(id) 27 | 28 | if (!ao) { 29 | throw new Error(`unknown algo order type: ${id}`) 30 | } 31 | 32 | const { meta = {} } = ao 33 | const { genOrderLabel } = meta 34 | 35 | let nextState = state 36 | 37 | for (let i = 0; i < _orders.length; i += 1) { 38 | const o = _orders[i] 39 | o.gid = +gid 40 | 41 | if (!o.meta) o.meta = {} 42 | 43 | if (_isFunction(genOrderLabel)) { 44 | o.meta.label = genOrderLabel(state) 45 | } 46 | 47 | o.meta._HF = 1 48 | 49 | debug( 50 | 'submitting order %s %f @ %f [gid %d]', 51 | o.type, o.amount, o.price, o.cid, o.gid 52 | ) 53 | 54 | nextState = await submitOrderWithDelay(nextState, delay, o) 55 | } 56 | 57 | return nextState 58 | }) 59 | } 60 | -------------------------------------------------------------------------------- /lib/host/events/update_state.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const withAOUpdate = require('../with_ao_update') 4 | 5 | /** 6 | * @param {Object} aoHost 7 | * @param {string} gid - AO instance GID to operate on 8 | * @param {string} update - state update 9 | */ 10 | module.exports = async (aoHost, gid, update = {}) => { 11 | await withAOUpdate(aoHost, gid, async (instance = {}) => { 12 | const { state = {} } = instance 13 | 14 | return { 15 | ...state, 16 | ...update 17 | } 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /lib/host/gen_helpers.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { 4 | cancelOrderWithDelay, submitOrderWithDelay 5 | } = require('bfx-api-node-core') 6 | 7 | const Promise = require('bluebird') 8 | const PI = require('p-iteration') 9 | const Debug = require('debug') 10 | 11 | /** 12 | * Generates a set of helpers to be used during algo order execution; these 13 | * helpers are saved on the AO instance and provided to all event handlers. 14 | * 15 | * @param {Object} state - algo order instance state 16 | * @return {Object} helpers 17 | */ 18 | module.exports = (state = {}) => { 19 | const { id, gid } = state 20 | const debug = Debug(`bfx:hf:algo:${id}:${gid}`) 21 | 22 | return { 23 | 24 | /** 25 | * Logs a string to the console, tagged by AO id/gid 26 | * 27 | * @param {string} str 28 | * @param {...any} args 29 | */ 30 | debug: (str, ...args) => { 31 | debug(str, ...args) 32 | }, 33 | 34 | /** 35 | * Triggeres an event on the 'self' section 36 | * 37 | * @example await emitSelf('submit_orders') 38 | * 39 | * @param {string} eventName 40 | * @param {...any} eventArgs 41 | */ 42 | emitSelf: async (eventName, ...eventArgs) => { 43 | debug('emit self:%s', eventName) 44 | await state.ev.emit(`self:${eventName}`, ...eventArgs) 45 | }, 46 | 47 | /** 48 | * Like `emitSelf` but operates after a timeout 49 | * 50 | * @param {string} eventName 51 | * @param {...any} eventArgs 52 | */ 53 | emitSelfAsync: async (eventName, ...eventArgs) => { 54 | return new Promise((resolve, reject) => { 55 | setTimeout(() => { 56 | debug('emit self:%s', eventName) 57 | 58 | state.ev 59 | .emit(`self:${eventName}`, ...eventArgs) 60 | .then(resolve) 61 | .catch(reject) 62 | }, 0) 63 | }) 64 | }, 65 | 66 | /** 67 | * Triggers a generic event 68 | * 69 | * @example await emit('exec:order:submit:all', gid, [order], submitDelay) 70 | * 71 | * @param {string} eventName 72 | * @param {...any} eventArgs 73 | */ 74 | emit: async (eventName, ...eventArgs) => { 75 | debug('emit %s', eventName) 76 | await state.ev.emit(eventName, ...eventArgs) 77 | }, 78 | 79 | /** 80 | * Like `emit` but operates after a timeout 81 | * 82 | * @param {string} eventName 83 | * @param {...any} eventArgs 84 | */ 85 | emitAsync: async (eventName, ...eventArgs) => { 86 | return new Promise((resolve, reject) => { 87 | setTimeout(() => { 88 | debug('emit %s', eventName) 89 | 90 | state.ev 91 | .emit(eventName, ...eventArgs) 92 | .then(resolve) 93 | .catch(reject) 94 | }, 0) 95 | }) 96 | }, 97 | 98 | /** 99 | * Triggers an UI notification, sent out via the active websocket connection 100 | * 101 | * @example await notifyUI('info', `Scheduled tick in ${delay}s`) 102 | * 103 | * @param {string} level - 'info', 'success', 'error', 'warning' 104 | * @param {string} message - notification content 105 | */ 106 | notifyUI: async (level, message) => { 107 | debug('notify %s: %s', level, message) 108 | await state.ev.emit('notify', gid, level, message) 109 | }, 110 | 111 | /** 112 | * Cancels the provided order after a delay, and removes it from the active 113 | * order set. 114 | * 115 | * @param {Object} state - current AO instance state 116 | * @param {number} delay - in ms 117 | * @param {Order} order 118 | * @return {Object} nextState 119 | */ 120 | cancelOrderWithDelay: async (state = {}, delay, order) => { 121 | const { ws, orders = {} } = state 122 | const { 123 | [order.cid + '']: knownOrder, 124 | ...otherOrders 125 | } = orders 126 | 127 | // NOTE: No await, in order to update cancelled order set immediately 128 | cancelOrderWithDelay(ws, delay, order) 129 | 130 | return { 131 | ...state, 132 | orders: otherOrders, 133 | cancelledOrders: { 134 | ...state.cancelledOrders, 135 | [order.cid + '']: order 136 | } 137 | } 138 | }, 139 | 140 | /** 141 | * Cancels all orders currently on the AO state after the specified delay 142 | * 143 | * @param {Object} state - current AO instance state 144 | * @param {number} delay - in ms 145 | * @return {Object} nextState 146 | */ 147 | cancelAllOrdersWithDelay: async (state = {}, delay) => { 148 | const { orders = {}, ws } = state 149 | 150 | // NOTE: No await, in order to update cancelled order set immediately 151 | PI.map(Object.values(orders), async (o) => { 152 | return cancelOrderWithDelay(ws, delay, o) 153 | }) 154 | 155 | return { 156 | ...state, 157 | 158 | orders: {}, 159 | cancelledOrders: { 160 | ...state.cancelledOrders, 161 | ...orders 162 | } 163 | } 164 | }, 165 | 166 | /** 167 | * Submits an order after a delay, and adds it to the active order set on 168 | * the AO state. 169 | * 170 | * @param {Object} state - current AO instance state 171 | * @param {number} delay 172 | * @param {Order} order 173 | * @return {Object} nextState 174 | */ 175 | submitOrderWithDelay: async (state = {}, delay, o) => { 176 | const { ws } = state 177 | const orderPatch = { 178 | [o.cid + '']: o 179 | } 180 | 181 | // Note that we don't wait for the order to submit here, since it might 182 | // fill immediately triggering a fill event before we patch the state 183 | // orders/allOrders objects 184 | submitOrderWithDelay(ws, delay, o).then((order) => { 185 | debug( 186 | 'order successfully submitted: %s %f @ %f %s', 187 | order.type, order.amountOrig, order.price, order.status 188 | ) 189 | }).catch(async (notification) => { 190 | debug('%s', notification.text) 191 | 192 | if (/minimum size/.test(notification.text)) { 193 | await state.ev.emit('error:minimum_size', gid, o, notification) 194 | } else if (/balance/.test(notification.text)) { 195 | await state.ev.emit('error:insufficient_balance', gid, o, notification) 196 | } 197 | }) 198 | 199 | return { 200 | ...state, 201 | 202 | allOrders: { // track beyond close 203 | ...state.allOrders, 204 | ...orderPatch 205 | }, 206 | 207 | orders: { 208 | ...state.orders, 209 | ...orderPatch 210 | } 211 | } 212 | }, 213 | 214 | /** 215 | * Hooks up the listener for a new event on the 'self' section 216 | * 217 | * @example declareEvent(instance, host, 'self:interval_tick', 'interval_tick') 218 | * 219 | * @param {Object} instance - full AO instance, with state/h 220 | * @param {Object} aoHost 221 | * @param {string} eventName 222 | * @param {string} path - on the 'self' section 223 | */ 224 | declareEvent: (instance = {}, aoHost, eventName, path) => { 225 | const { state = {} } = instance 226 | const { ev } = state 227 | const handler = aoHost.onAOSelfEvent.bind(aoHost, instance, path) 228 | 229 | ev.on(eventName, handler) 230 | }, 231 | 232 | /** 233 | * Assigns a data channel to the provided AO instance 234 | * 235 | * @example await declareChannel(instance, host, 'trades', { symbol }) 236 | * 237 | * @param {Object} instance - full AO instance, with state/h 238 | * @param {Object} aoHost 239 | * @param {string} channel - channel name, i.e. 'ticker' 240 | * @param {Object} filter - channel spec, i.e. { symbol: 'tBTCUSD' } 241 | * @return {Object} nextState 242 | */ 243 | declareChannel: async (instance = {}, aoHost, channel, filter) => { 244 | const { h = {}, state = {} } = instance 245 | const { gid } = state 246 | const { emit } = h 247 | 248 | await emit('channel:assign', gid, channel, filter) 249 | 250 | return instance.state // updated ref 251 | }, 252 | 253 | /** 254 | * Updates the state for the provided AO instance 255 | * 256 | * @param {Object} instance - full AO instance, with state/h 257 | * @param {Object} update - new state 258 | * @return {Object} nextState 259 | */ 260 | updateState: async (instance = {}, update = {}) => { 261 | const { h = {}, state = {} } = instance 262 | const { gid } = state 263 | const { emit } = h 264 | 265 | await emit('state:update', gid, update) 266 | 267 | return instance.state // updated ref 268 | } 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /lib/host/init_ao.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const initAOState = require('./init_ao_state') 4 | const genHelpers = require('./gen_helpers') 5 | 6 | /** 7 | * Creates a new algo order instance from the provided definition object & 8 | * arguments. 9 | * 10 | * @param {Object} aoDef - algo order definition 11 | * @param {Object} args - instance arguments 12 | * @return {Object} instance 13 | */ 14 | module.exports = (aoDef = {}, args = {}) => { 15 | const state = initAOState(aoDef, args) 16 | const h = genHelpers(state) 17 | 18 | return { state, h } 19 | } 20 | -------------------------------------------------------------------------------- /lib/host/init_ao_state.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { nonce } = require('bfx-api-node-util') 4 | const _isFunction = require('lodash/isFunction') 5 | const AsyncEventEmitter = require('../async_event_emitter') 6 | 7 | /** 8 | * Creates the initial state object for an algo order, after processing 9 | * and validating the provided arguments. 10 | * 11 | * @param {Object} aoDef - algo order definition object 12 | * @param {Object} args - instance arguments 13 | * @return {Object} initialState 14 | */ 15 | module.exports = (aoDef = {}, args = {}) => { 16 | const { meta = {}, id } = aoDef 17 | const { validateParams, processParams, initState } = meta 18 | const params = _isFunction(processParams) 19 | ? processParams(args) 20 | : args 21 | 22 | if (_isFunction(validateParams)) { 23 | const vError = validateParams(params) 24 | 25 | if (vError) { 26 | throw new Error(vError) 27 | } 28 | } 29 | 30 | const initialState = _isFunction(initState) 31 | ? initState(params) 32 | : {} 33 | 34 | const gid = nonce() + '' 35 | 36 | return { 37 | channels: [], 38 | orders: {}, // active 39 | cancelledOrders: {}, // cancelled by us (not via UI) 40 | allOrders: {}, // active + closed 41 | id: id, 42 | gid: gid, 43 | ev: new AsyncEventEmitter(), 44 | 45 | ...initialState 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/host/ui/register_ao_uis.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const debug = require('debug')('bfx:hf:host:register-ao-uis') 4 | const _isFunction = require('lodash/isFunction') 5 | 6 | const { PLATFORM = 'bitfinex' } = process.env 7 | 8 | /** 9 | * Registers all order form layouts for the provided AO Host 10 | * 11 | * @param {Object} aoHost - algo order host 12 | */ 13 | module.exports = async (aoHost = {}) => { 14 | const { aos, m } = aoHost 15 | const { rest } = m 16 | const uis = Object.values(aos).filter((ao = {}) => { 17 | const { meta = {} } = ao 18 | const { getUIDef } = meta 19 | 20 | return _isFunction(getUIDef) 21 | }).map((ao = {}) => { 22 | const { meta = {} } = ao 23 | const { getUIDef } = meta 24 | const { id } = ao 25 | 26 | return { id, getUIDef } 27 | }) 28 | 29 | if (uis.length === 0) { 30 | debug('no UIs to register') 31 | return 32 | } 33 | 34 | return rest.getSettings([`api:${PLATFORM}_algorithmic_orders`]).then((res = []) => { 35 | const [keyResult = []] = res 36 | const [, aoSettings = {}] = keyResult 37 | 38 | uis.forEach(({ id, getUIDef }) => { 39 | debug('registering UI %s', id) 40 | aoSettings[id] = getUIDef() 41 | }) 42 | 43 | return rest.updateSettings({ 44 | [`api:${PLATFORM}_algorithmic_orders`]: aoSettings 45 | }) 46 | }).then(() => { 47 | debug('all UIs registered!') 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /lib/host/with_ao_update.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const _isObject = require('lodash/isObject') 4 | 5 | /** 6 | * Calls the provided cb with the current instance state by gid, and 7 | * saves the result as the new instance state. 8 | * 9 | * @param {Object} aoState 10 | * @param {string} gid - AO instance gid 11 | * @param {Function} cb - async method to call 12 | * @return {Object} nextInstanceState 13 | */ 14 | module.exports = async (aoHost, gid, cb) => { 15 | const { instances } = aoHost 16 | 17 | if (!instances[gid]) { 18 | return 19 | } 20 | 21 | const state = await cb(instances[gid]) 22 | 23 | if (_isObject(state)) { 24 | instances[gid].state = state 25 | 26 | await aoHost.emit('ao:persist', gid) 27 | } 28 | 29 | return state 30 | } 31 | -------------------------------------------------------------------------------- /lib/host/ws2/bind_bus.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const debug = require('debug')('bfx:hf:host:ws2:bind-bus') 4 | const _isEmpty = require('lodash/isEmpty') 5 | const processMessage = require('./process_message') 6 | 7 | module.exports = (aoHost) => { 8 | const { m } = aoHost 9 | const messages = [] 10 | let processing = false 11 | 12 | const enqueMessage = (type, ...args) => { 13 | debug('enqueue %s', type) 14 | 15 | messages.push({ type, args }) 16 | 17 | if (!processing) { 18 | processMessages().catch((err) => { 19 | debug('error processing: %s', err.stack) 20 | }) 21 | } 22 | } 23 | 24 | const processMessages = async () => { 25 | processing = true 26 | 27 | while (!_isEmpty(messages)) { 28 | const [ msg ] = messages.splice(0, 1) 29 | 30 | await processMessage(aoHost, msg) 31 | } 32 | 33 | processing = false 34 | } 35 | 36 | [ 37 | 'ws2:open', 38 | 'ws2:event:auth:success', 39 | 'ws2:event:auth:error', 40 | 'ws2:auth:os', 41 | 'ws2:auth:on', 42 | 'ws2:auth:ou', 43 | 'ws2:auth:oc', 44 | 'ws2:auth:n', 45 | 'ws2:data:trades', 46 | 'ws2:data:book' 47 | ].forEach((msgType) => { 48 | m.on(msgType, (...args) => { 49 | enqueMessage(msgType, ...args) 50 | }) 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /lib/host/ws2/process_message.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const debug = require('debug')('bfx:hf:host:ws2:process-message') 4 | 5 | module.exports = async (aoHost, msg = {}) => { 6 | const { triggerGlobalEvent, triggerOrderEvent } = aoHost 7 | const { type, args } = msg 8 | 9 | switch (type) { 10 | case 'ws2:open': { 11 | debug('process %s', type) 12 | 13 | await aoHost.emit('open') 14 | break 15 | } 16 | 17 | case 'ws2:event:auth:success': { 18 | debug('process %s', type) 19 | 20 | const [ packet, meta ] = args 21 | await aoHost.emit('ws2:auth:success', packet, meta) 22 | break 23 | } 24 | 25 | case 'ws2:event:auth:error': { 26 | debug('process %s', type) 27 | 28 | const [ packet, meta ] = args 29 | await aoHost.emit('ws2:auth:error', packet, meta) 30 | break 31 | } 32 | 33 | case 'ws2:auth:n': { 34 | debug('process %s', type) 35 | 36 | const [ packet, meta ] = args 37 | await aoHost.emit('ws2:auth:n', packet, meta) 38 | break 39 | } 40 | 41 | case 'ws2:auth:os': { 42 | debug('process %s', type) 43 | 44 | const [ orders ] = args 45 | await triggerGlobalEvent('orders', 'order_snapshot', orders) 46 | break 47 | } 48 | 49 | case 'ws2:auth:on': { 50 | const [ order ] = args 51 | const { amount, amountOrig, price, status } = order 52 | 53 | debug( 54 | 'process %s [%f/%f @ %f %s]', 55 | type, amount, amountOrig, price, status 56 | ) 57 | 58 | await triggerOrderEvent('orders', 'order_new', order) 59 | 60 | if (status.match(/PARTIALLY/)) { 61 | await triggerOrderEvent('orders', 'order_fill', order) 62 | } 63 | 64 | break 65 | } 66 | 67 | case 'ws2:auth:ou': { 68 | const [ order ] = args 69 | const { amount, amountOrig, price, status } = order 70 | 71 | debug( 72 | 'process %s [%f/%f @ %f %s]', 73 | type, amount, amountOrig, price, status 74 | ) 75 | 76 | await triggerOrderEvent('orders', 'order_update', order) 77 | 78 | if (status.match(/PARTIALLY/)) { 79 | await triggerOrderEvent('orders', 'order_fill', order) 80 | } 81 | 82 | break 83 | } 84 | 85 | case 'ws2:auth:oc': { 86 | const [ order ] = args 87 | const { amount, amountOrig, price, status } = order 88 | 89 | debug( 90 | 'process %s [%f/%f @ %f %s]', 91 | type, amount, amountOrig, price, status 92 | ) 93 | 94 | await triggerOrderEvent('orders', 'order_close', order) 95 | 96 | if (status.match(/CANCELED/)) { 97 | await triggerOrderEvent('orders', 'order_cancel', order) 98 | } else { 99 | await triggerOrderEvent('orders', 'order_fill', order) 100 | } 101 | 102 | break 103 | } 104 | 105 | case 'ws2:data:trades': { 106 | debug('process %s', type) 107 | 108 | const [ trades ] = args 109 | await triggerGlobalEvent('data', 'trades', trades) 110 | break 111 | } 112 | 113 | case 'ws2:data:book': { 114 | debug('process %s', type) 115 | 116 | const [ update ] = args 117 | await triggerGlobalEvent('data', 'book', update) 118 | break 119 | } 120 | 121 | default: { 122 | debug('unknown ws event: %s [%j]', type, args) 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /lib/iceberg/events/error_minimum_size.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * Cancel the AO if we try to submit an order below the minimum size, since it 5 | * means the remaining amount is below the min size (and therefore cannot fill) 6 | * 7 | * @param {Object} instance 8 | * @param {Order} order - order which is below the min size for its symbol 9 | */ 10 | module.exports = async (instance = {}, o) => { 11 | const { state = {}, h = {} } = instance 12 | const { gid, args = {}, orders = {} } = state 13 | const { emit, debug } = h 14 | const { cancelDelay } = args 15 | 16 | debug('received minimum size error for order: %f @ %f', o.amountOrig, o.price) 17 | debug('stopping order...') 18 | 19 | await emit('exec:order:cancel:all', gid, orders, cancelDelay) 20 | await emit('exec:stop') 21 | } 22 | -------------------------------------------------------------------------------- /lib/iceberg/events/insufficient_balance.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * Cancel the AO if we try to submit an order without having enough balance 5 | * 6 | * @param {Object} instance 7 | * @param {Order} order - order which is below the min size for its symbol 8 | */ 9 | module.exports = async (instance = {}, o) => { 10 | const { state = {}, h = {} } = instance 11 | const { gid, args = {}, orders = {} } = state 12 | const { emit, debug } = h 13 | const { cancelDelay } = args 14 | 15 | debug('received insufficient balance error for order: %f @ %f', o.amountOrig, o.price) 16 | debug('stopping order...') 17 | 18 | await emit('exec:order:cancel:all', gid, orders, cancelDelay) 19 | await emit('exec:stop') 20 | } 21 | -------------------------------------------------------------------------------- /lib/iceberg/events/life_start.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const _debounce = require('lodash/debounce') 4 | 5 | module.exports = async (instance = {}) => { 6 | const { h = {} } = instance 7 | const { emitSelf } = h 8 | 9 | // Needs to be debounced, in case both orders are filled simultaneously, 10 | // triggering two submits in a row 11 | h.debouncedSubmitOrders = _debounce(() => { 12 | emitSelf('submit_orders') 13 | }, 500) 14 | 15 | await emitSelf('submit_orders') 16 | } 17 | -------------------------------------------------------------------------------- /lib/iceberg/events/life_stop.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = async (instance = {}) => { 4 | const { state = {}, h = {} } = instance 5 | const { args = {}, orders = {}, gid } = state 6 | const { emit, debouncedSubmitOrders } = h 7 | const { cancelDelay } = args 8 | 9 | debouncedSubmitOrders.cancel() 10 | 11 | await emit('exec:order:cancel:all', gid, orders, cancelDelay) 12 | } 13 | -------------------------------------------------------------------------------- /lib/iceberg/events/orders_order_cancel.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = async (instance = {}, order) => { 4 | const { state = {}, h = {} } = instance 5 | const { args = {}, orders = {}, gid } = state 6 | const { emit, debug } = h 7 | const { cancelDelay } = args 8 | 9 | debug('detected atomic cancelation, stopping...') 10 | 11 | await emit('exec:order:cancel:all', gid, orders, cancelDelay) 12 | await emit('exec:stop') 13 | } 14 | -------------------------------------------------------------------------------- /lib/iceberg/events/orders_order_fill.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { Config } = require('bfx-api-node-core') 4 | const { DUST } = Config 5 | 6 | module.exports = async (instance = {}, order) => { 7 | const { state = {}, h = {} } = instance 8 | const { args = {}, orders = {}, gid } = state 9 | const { emit, updateState, debug, debouncedSubmitOrders } = h 10 | const { cancelDelay, amount } = args 11 | const m = amount < 0 ? -1 : 1 12 | 13 | await emit('exec:order:cancel:all', gid, orders, cancelDelay) 14 | 15 | const fillAmount = order.getLastFillAmount() 16 | const remainingAmount = state.remainingAmount - fillAmount 17 | const absRem = m < 0 ? remainingAmount * -1 : remainingAmount 18 | 19 | order.resetFilledAmount() 20 | 21 | debug('updated remaining amount: %f [filled %f]', remainingAmount, fillAmount) 22 | 23 | await updateState(instance, { remainingAmount }) 24 | 25 | if (absRem > DUST) { // continue 26 | debouncedSubmitOrders() // created in life.start 27 | return 28 | } 29 | 30 | if (absRem < 0) { 31 | debug('warning: overfill! %f', absRem) 32 | } 33 | 34 | await emit('exec:stop') 35 | } 36 | -------------------------------------------------------------------------------- /lib/iceberg/events/self_submit_orders.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const generateOrders = require('../util/generate_orders') 4 | 5 | module.exports = async (instance = {}) => { 6 | const { state = {}, h = {} } = instance 7 | const { emit } = h 8 | const { args = {}, gid } = state 9 | const { submitDelay } = args 10 | const orders = generateOrders(state) 11 | 12 | await emit('exec:order:submit:all', gid, orders, submitDelay) 13 | } 14 | -------------------------------------------------------------------------------- /lib/iceberg/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const defineAlgoOrder = require('../define_algo_order') 4 | const validateParams = require('./meta/validate_params') 5 | const processParams = require('./meta/process_params') 6 | const initState = require('./meta/init_state') 7 | const onSelfSubmitOrders = require('./events/self_submit_orders') 8 | const onLifeStart = require('./events/life_start') 9 | const onLifeStop = require('./events/life_stop') 10 | const onOrdersOrderFill = require('./events/orders_order_fill') 11 | const onOrdersOrderCancel = require('./events/orders_order_cancel') 12 | const onMinimumSizeError = require('./events/error_minimum_size') 13 | const onInsufficientBalanceError = require('./events/insufficient_balance') 14 | const genPreview = require('./meta/gen_preview') 15 | const declareEvents = require('./meta/declare_events') 16 | const getUIDef = require('./meta/get_ui_def') 17 | const serialize = require('./meta/serialize') 18 | const unserialize = require('./meta/unserialize') 19 | const genOrderLabel = require('./meta/gen_order_label') 20 | 21 | /** 22 | * Iceberg allows you to place a large order on the market while ensuring only 23 | * a small part of it is ever filled at once. By enabling the 'Excess As Hidden' 24 | * option, it is possible to offer up the remainder as a hidden order, allowing 25 | * for minimal market disruption when executing large trades. 26 | * 27 | * @name Iceberg 28 | * @param {string} symbol - symbol to trade on 29 | * @param {number} amount - total order amount 30 | * @param {number} sliceAmount - iceberg slice order amount 31 | * @param {number?} sliceAmountPerc - optional, slice amount as % of total amount 32 | * @param {boolean} excessAsHidden - whether to submit remainder as a hidden order 33 | * @param {string} orderType - LIMIT or MARKET 34 | * @param {number?} submitDelay - in ms, default 1500 35 | * @param {number?} cancelDelay - in ms, default 5000 36 | * @param {boolean?} _margin - if false, prefixes order type with EXCHANGE 37 | */ 38 | module.exports = defineAlgoOrder({ 39 | id: 'bfx.iceberg', 40 | name: 'Iceberg', 41 | 42 | meta: { 43 | genOrderLabel, 44 | validateParams, 45 | processParams, 46 | declareEvents, 47 | genPreview, 48 | initState, 49 | getUIDef, 50 | serialize, 51 | unserialize 52 | }, 53 | 54 | events: { 55 | self: { 56 | submit_orders: onSelfSubmitOrders 57 | }, 58 | 59 | life: { 60 | start: onLifeStart, 61 | stop: onLifeStop 62 | }, 63 | 64 | orders: { 65 | order_fill: onOrdersOrderFill, 66 | order_cancel: onOrdersOrderCancel 67 | }, 68 | 69 | errors: { 70 | minimum_size: onMinimumSizeError, 71 | insufficient_balance: onInsufficientBalanceError 72 | } 73 | } 74 | }) 75 | -------------------------------------------------------------------------------- /lib/iceberg/meta/declare_events.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = (instance = {}, host) => { 4 | const { h = {} } = instance 5 | const { declareEvent } = h 6 | 7 | declareEvent(instance, host, 'self:submit_orders', 'submit_orders') 8 | } 9 | -------------------------------------------------------------------------------- /lib/iceberg/meta/gen_order_label.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = (state = {}) => { 4 | const { args = {} } = state 5 | const { amount, price, sliceAmount, excessAsHidden } = args 6 | const mul = amount < 0 ? -1 : 1 7 | 8 | return [ 9 | 'Iceberg', 10 | ` | ${amount} @ ${price} `, 11 | ` | slice ${mul * sliceAmount}`, 12 | 13 | excessAsHidden 14 | ? ` | excess ${mul * (Math.abs(amount) - Math.abs(sliceAmount))}` 15 | : '' 16 | ].join('') 17 | } 18 | -------------------------------------------------------------------------------- /lib/iceberg/meta/gen_preview.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const genOrders = require('../util/generate_orders') 4 | 5 | module.exports = (args = {}) => { 6 | const { amount } = args 7 | 8 | return genOrders({ 9 | remainingAmount: amount, 10 | args 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /lib/iceberg/meta/get_ui_def.js: -------------------------------------------------------------------------------- 1 | module.exports = () => ({ 2 | label: 'Iceberg', 3 | customHelp: 'Iceberg allows you to place a large order on the market while ensuring only a small part of it is ever filled at once.\n\nBy enabling the \'Excess As Hidden\' option, it is possible to offer up the remainder as a hidden order, allowing for minimal market disruption when executing large trades.', 4 | connectionTimeout: 10000, 5 | actionTimeout: 10000, 6 | 7 | sections: [{ 8 | title: '', 9 | name: 'general', 10 | rows: [ 11 | ['orderType', 'price'], 12 | ['amount', 'excessAsHidden'], 13 | ['sliceAmount', 'sliceAmountPerc'], 14 | ['submitDelaySec', 'cancelDelaySec'], 15 | ['action', null] 16 | ] 17 | }], 18 | 19 | fields: { 20 | excessAsHidden: { 21 | component: 'input.checkbox', 22 | label: 'Excess as hidden', 23 | default: true, 24 | customHelp: 'Create a hidden order for the non-slice amount' 25 | }, 26 | 27 | orderType: { 28 | component: 'input.dropdown', 29 | label: 'Order Type', 30 | default: 'LIMIT', 31 | options: { 32 | LIMIT: 'Limit', 33 | MARKET: 'Market' 34 | } 35 | }, 36 | 37 | price: { 38 | component: 'input.price', 39 | label: 'Price $QUOTE', 40 | disabled: { 41 | orderType: { eq: 'MARKET' } 42 | } 43 | }, 44 | 45 | amount: { 46 | component: 'input.amount', 47 | label: 'Amount $BASE', 48 | customHelp: 'Total order amount, to be executed slice-by-slice', 49 | priceField: 'price' 50 | }, 51 | 52 | sliceAmount: { 53 | component: 'input.number', 54 | label: 'Slice Amount $BASE', 55 | customHelp: 'Allows individual buy & sell amounts to be adjusted' 56 | }, 57 | 58 | sliceAmountPerc: { 59 | component: 'input.percent', 60 | label: 'Slice Amount as %', 61 | customHelp: 'Takes priority over literal amount' 62 | }, 63 | 64 | submitDelaySec: { 65 | component: 'input.number', 66 | label: 'Submit Delay (sec)', 67 | customHelp: 'Seconds to wait before submitting orders', 68 | default: 2 69 | }, 70 | 71 | cancelDelaySec: { 72 | component: 'input.number', 73 | label: 'Cancel Delay (sec)', 74 | customHelp: 'Seconds to wait before cancelling orders', 75 | default: 1 76 | }, 77 | 78 | action: { 79 | component: 'input.radio', 80 | label: 'Action', 81 | options: ['Buy', 'Sell'], 82 | inline: true, 83 | default: 'Buy' 84 | } 85 | }, 86 | 87 | actions: ['preview', 'submit'] 88 | }) 89 | -------------------------------------------------------------------------------- /lib/iceberg/meta/init_state.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = (args = {}) => { 4 | const { amount } = args 5 | 6 | return { 7 | remainingAmount: amount, 8 | args 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/iceberg/meta/process_params.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = (data) => { 4 | const params = { ...data } 5 | 6 | if (params.orderType && !params._margin) { 7 | params.orderType = `EXCHANGE ${params.orderType}` 8 | } 9 | 10 | if (params._symbol) { 11 | params.symbol = params._symbol 12 | delete params._symbol 13 | } 14 | 15 | if (params.cancelDelaySec) { 16 | params.cancelDelay = params.cancelDelaySec * 1000 17 | delete params.cancelDelaySec 18 | } 19 | 20 | if (params.submitDelaySec) { 21 | params.submitDelay = params.submitDelaySec * 1000 22 | delete params.submitDelaySec 23 | } 24 | 25 | if (!params.cancelDelay) { 26 | params.cancelDelay = 1000 27 | } 28 | 29 | if (!params.submitDelay) { 30 | params.submitDelay = 2000 31 | } 32 | 33 | if (params.sliceAmountPerc) { 34 | params.sliceAmount = params.amount * (+params.sliceAmountPerc) 35 | delete params.sliceAmountPerc 36 | } 37 | 38 | if (params.action) { 39 | if (params.action === 'Sell') { 40 | params.amount = Number(params.amount) * -1 41 | params.sliceAmount = Number(params.sliceAmount) * -1 42 | } 43 | 44 | delete params.action 45 | } 46 | 47 | return params 48 | } 49 | -------------------------------------------------------------------------------- /lib/iceberg/meta/serialize.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = (state = {}) => { 4 | const { remainingAmount, args = {} } = state 5 | 6 | return { 7 | remainingAmount, 8 | args 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/iceberg/meta/unserialize.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = (loadedState = {}) => { 4 | const { remainingAmount, args = {} } = loadedState 5 | 6 | return { 7 | remainingAmount, 8 | args 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/iceberg/meta/validate_params.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { Order } = require('bfx-api-node-models') 4 | const _isFinite = require('lodash/isFinite') 5 | 6 | module.exports = (args = {}) => { 7 | const { 8 | price, amount, sliceAmount, orderType, submitDelay, cancelDelay 9 | } = args 10 | 11 | if (!Order.type[orderType]) return `Invalid order type: ${orderType}` 12 | if (!_isFinite(amount)) return 'Invalid amount' 13 | if (!_isFinite(sliceAmount)) return 'Invalid slice amount' 14 | if (!_isFinite(submitDelay) || submitDelay < 0) return 'Invalid submit delay' 15 | if (!_isFinite(cancelDelay) || cancelDelay < 0) return 'Invalid cancel delay' 16 | if ((orderType.indexOf('MARKET') === -1) && (isNaN(price) || price <= 0)) { 17 | return 'Invalid price' 18 | } 19 | 20 | if ( 21 | (amount < 0 && sliceAmount >= 0) || 22 | (amount > 0 && sliceAmount <= 0) 23 | ) { 24 | return 'Amount & slice amount must have same sign' 25 | } 26 | 27 | return null 28 | } 29 | -------------------------------------------------------------------------------- /lib/iceberg/util/generate_orders.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { Order } = require('bfx-api-node-models') 4 | const { nonce } = require('bfx-api-node-util') 5 | const { Config } = require('bfx-api-node-core') 6 | const { DUST } = Config 7 | 8 | module.exports = (state = {}) => { 9 | const { args = {}, remainingAmount } = state 10 | const { 11 | sliceAmount, price, excessAsHidden, orderType, symbol, amount 12 | } = args 13 | const m = amount < 0 ? -1 : 1 14 | const orders = [] 15 | const sliceOrderAmount = m === 1 16 | ? Math.min(sliceAmount, remainingAmount) 17 | : Math.max(sliceAmount, remainingAmount) 18 | 19 | if (Math.abs(sliceOrderAmount) <= DUST) { 20 | return [] 21 | } 22 | 23 | if (excessAsHidden) { 24 | const rem = remainingAmount - sliceAmount 25 | 26 | if ( 27 | (m === 1 && rem >= DUST) || 28 | (m === -1 && rem <= DUST) 29 | ) { 30 | orders.push(new Order({ 31 | symbol, 32 | price, 33 | cid: nonce(), 34 | type: orderType, 35 | amount: rem, 36 | hidden: true 37 | })) 38 | } 39 | } 40 | 41 | orders.push(new Order({ 42 | symbol, 43 | price, 44 | cid: nonce(), 45 | type: orderType, 46 | amount: sliceOrderAmount 47 | })) 48 | 49 | return orders 50 | } 51 | -------------------------------------------------------------------------------- /lib/ping_pong/events/error_minimum_size.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = async (instance = {}, o) => { 4 | const { h = {} } = instance 5 | const { emit, debug } = h 6 | 7 | debug('received minimum size error for order: %f @ %f', o.amountOrig, o.price) 8 | debug('stopping order...') 9 | 10 | await emit('exec:stop') 11 | } 12 | -------------------------------------------------------------------------------- /lib/ping_pong/events/insufficient_balance.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * Cancel the AO if we try to submit an order without having enough balance 5 | * 6 | * @param {Object} instance 7 | * @param {Order} order - order which is below the min size for its symbol 8 | */ 9 | module.exports = async (instance = {}, o) => { 10 | const { state = {}, h = {} } = instance 11 | const { gid, args = {}, orders = {} } = state 12 | const { emit, debug } = h 13 | const { cancelDelay } = args 14 | 15 | debug('received insufficient balance error for order: %f @ %f', o.amountOrig, o.price) 16 | debug('stopping order...') 17 | 18 | await emit('exec:order:cancel:all', gid, orders, cancelDelay) 19 | await emit('exec:stop') 20 | } 21 | -------------------------------------------------------------------------------- /lib/ping_pong/events/life_start.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { nonce } = require('bfx-api-node-util') 4 | const { Order } = require('bfx-api-node-models') 5 | const _isEmpty = require('lodash/isEmpty') 6 | 7 | module.exports = async (instance = {}) => { 8 | const { state = {}, h = {} } = instance 9 | const { emit, debug } = h 10 | const { args = {}, gid, pingPongTable, activePongs } = state 11 | const { amount, submitDelay, symbol, hidden, _margin } = args 12 | 13 | const pingPrices = Object.keys(pingPongTable) 14 | const orders = pingPrices.map(price => ( 15 | new Order({ 16 | symbol, 17 | price, 18 | cid: nonce(), 19 | gid, 20 | type: _margin ? Order.type.LIMIT : Order.type.EXCHANGE_LIMIT, 21 | amount, 22 | hidden, 23 | }) 24 | )) 25 | 26 | debug('submitting ping orders: [%j]', pingPrices) 27 | await emit('exec:order:submit:all', gid, orders, submitDelay) 28 | 29 | if (_isEmpty(activePongs)) { 30 | return 31 | } 32 | 33 | // Handle saved pongs 34 | const pongOrders = Object.keys(activePongs).map(price => ( 35 | new Order({ 36 | symbol, 37 | price, 38 | cid: nonce(), 39 | gid, 40 | type: _margin ? Order.type.LIMIT : Order.type.EXCHANGE_LIMIT, 41 | amount: -amount, 42 | hidden, 43 | }) 44 | )) 45 | 46 | debug('submitting pong orders: [%j]', Object.keys(activePongs)) 47 | await emit('exec:order:submit:all', gid, pongOrders, submitDelay) 48 | } 49 | -------------------------------------------------------------------------------- /lib/ping_pong/events/life_stop.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = async (instance = {}) => { 4 | const { state = {}, h = {} } = instance 5 | const { args = {}, orders = {}, gid } = state 6 | const { emit } = h 7 | const { cancelDelay } = args 8 | 9 | await emit('exec:order:cancel:all', gid, orders, cancelDelay) 10 | } 11 | -------------------------------------------------------------------------------- /lib/ping_pong/events/orders_order_cancel.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = async (instance = {}, order) => { 4 | const { state = {}, h = {} } = instance 5 | const { args = {}, orders = {}, gid } = state 6 | const { emit, debug } = h 7 | const { cancelDelay } = args 8 | 9 | debug('detected atomic cancelation, stopping...') 10 | 11 | await emit('exec:order:cancel:all', gid, orders, cancelDelay) 12 | await emit('exec:stop') 13 | } 14 | -------------------------------------------------------------------------------- /lib/ping_pong/events/orders_order_fill.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { Order } = require('bfx-api-node-models') 4 | const { nonce } = require('bfx-api-node-util') 5 | const { Config } = require('bfx-api-node-core') 6 | const { DUST } = Config 7 | 8 | module.exports = async (instance = {}, order) => { 9 | const { state = {}, h = {} } = instance 10 | const { args = {}, gid, pingPongTable, activePongs } = state 11 | const { emit, debug, updateState } = h 12 | const { endless, submitDelay, symbol, amount, hidden, _margin } = args 13 | const { price } = order 14 | 15 | const { 16 | [price]: pongPrice, 17 | ...nextPingPongTable 18 | } = pingPongTable 19 | 20 | if (!pongPrice) { 21 | const { 22 | [price]: pingPrice, 23 | ...nextActivePongs 24 | } = activePongs 25 | 26 | if (pingPrice) { 27 | debug('pong filled: %f', price) 28 | 29 | // NOTE: Shadows from above 30 | const nextPingPongTable = !endless 31 | ? pingPongTable 32 | : { 33 | ...pingPongTable, 34 | [pingPrice]: price, 35 | } 36 | 37 | await updateState(instance, { 38 | activePongs: nextActivePongs, 39 | pingPongTable: nextPingPongTable 40 | }) 41 | 42 | if (endless) { 43 | const pingOrder = new Order({ 44 | symbol, 45 | price: pingPrice, 46 | cid: nonce(), 47 | gid, 48 | type: _margin ? Order.type.LIMIT : Order.type.EXCHANGE_LIMIT, 49 | amount, 50 | hidden, 51 | }) 52 | 53 | await emit('exec:order:submit:all', gid, [pingOrder], submitDelay) 54 | 55 | } else if ( 56 | Object.keys(pingPongTable).length === 0 && 57 | Object.keys(nextActivePongs).length === 0 58 | ) { 59 | debug('all orders filled') 60 | await emit('exec:stop') 61 | } 62 | } 63 | 64 | return 65 | } 66 | 67 | if (order.amount > DUST) { // not fully filled 68 | return 69 | } 70 | 71 | const pongOrder = new Order({ 72 | symbol, 73 | price: pongPrice, 74 | cid: nonce(), 75 | gid, 76 | type: _margin ? Order.type.LIMIT : Order.type.EXCHANGE_LIMIT, 77 | amount: -amount, 78 | hidden, 79 | }) 80 | 81 | debug('submitting pong order %f for ping %f', pongPrice, price) 82 | 83 | await emit('exec:order:submit:all', gid, [pongOrder], submitDelay) 84 | await updateState(instance, { 85 | pingPongTable: nextPingPongTable, 86 | activePongs: { 87 | ...activePongs, 88 | [pongPrice]: price 89 | } 90 | }) 91 | } 92 | -------------------------------------------------------------------------------- /lib/ping_pong/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const defineAlgoOrder = require('../define_algo_order') 4 | const validateParams = require('./meta/validate_params') 5 | const genPreview = require('./meta/gen_preview') 6 | const processParams = require('./meta/process_params') 7 | const onLifeStart = require('./events/life_start') 8 | const onLifeStop = require('./events/life_stop') 9 | const onOrdersOrderFill = require('./events/orders_order_fill') 10 | const onOrdersOrderCancel = require('./events/orders_order_cancel') 11 | const onMinimumSizeError = require('./events/error_minimum_size') 12 | const onInsufficientBalanceError = require('./events/insufficient_balance') 13 | const initState = require('./meta/init_state') 14 | const getUIDef = require('./meta/get_ui_def') 15 | const serialize = require('./meta/serialize') 16 | const unserialize = require('./meta/unserialize') 17 | const genOrderLabel = require('./meta/gen_order_label') 18 | 19 | /** 20 | * Ping/pong submits multiple 'ping' orders; once a ping order fills, an 21 | * associated 'pong' order is submitted. 22 | * 23 | * Multiple ping/pong pairs can be created by specifying an order count greater 24 | * than 1, a suitable min/max ping price, and a pong distance. Multiple ping 25 | * orders will be created between the specified min/max prices, with the 26 | * associated pongs offset by the pong distance from the ping price. 27 | * 28 | * When operating in 'endless' mode, new ping orders will be submitted when 29 | * their associated pongs fill. 30 | * 31 | * @name PingPong 32 | * @param {boolean} endless - if enabled, pong fill will trigger a new ping 33 | * @param {string} symbol - symbol to trade on 34 | * @param {number} amount - individual ping/pong order amount 35 | * @param {number} orderCount - number of ping/pong pairs to create, 1 or more 36 | * @param {number?} pingPrice - used for a single ping/pong pair 37 | * @param {number?} pongPrice - used for a single ping/pong pair 38 | * @param {number?} pingMinPrice - minimum price for ping orders 39 | * @param {number?} pingMaxPrice - maximum price for ping orders 40 | * @param {number?} pongDistance - pong offset from ping orders for multiple pairs 41 | */ 42 | module.exports = defineAlgoOrder({ 43 | id: 'bfx.ping_pong', 44 | name: 'Ping/Pong', 45 | 46 | meta: { 47 | genOrderLabel, 48 | validateParams, 49 | processParams, 50 | genPreview, 51 | initState, 52 | getUIDef, 53 | serialize, 54 | unserialize 55 | }, 56 | 57 | events: { 58 | life: { 59 | start: onLifeStart, 60 | stop: onLifeStop 61 | }, 62 | 63 | orders: { 64 | order_fill: onOrdersOrderFill, 65 | order_cancel: onOrdersOrderCancel 66 | }, 67 | 68 | errors: { 69 | minimum_size: onMinimumSizeError, 70 | insufficient_balance: onInsufficientBalanceError 71 | } 72 | } 73 | }) 74 | -------------------------------------------------------------------------------- /lib/ping_pong/meta/gen_order_label.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = (state = {}) => { 4 | const { args = {} } = state 5 | const { 6 | amount, pingPrice, pongPrice, orderCount, pingMinPrice, pingMaxPrice, 7 | pongDistance 8 | } = args 9 | 10 | if (orderCount === 1) { 11 | return `Ping/Pong | ${amount} @ ${pingPrice} -> ${pongPrice} ` 12 | } else { 13 | const sign = amount < 0 ? '-' : '+' 14 | const spread = `[${pingMinPrice}..${pingMaxPrice}]` 15 | return `Ping/Pong | ${amount} @ ${spread} -> ${sign}${pongDistance} ` 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/ping_pong/meta/gen_preview.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { nonce } = require('bfx-api-node-util') 4 | const { Order } = require('bfx-api-node-models') 5 | const { preparePrice } = require('bfx-api-node-util') 6 | const genPingPongTable = require('../util/gen_ping_pong_table') 7 | 8 | module.exports = (args = {}) => { 9 | const { endless, hidden, amount, symbol, _margin } = args 10 | const pingPongTable = genPingPongTable(args) 11 | const pings = Object.keys(pingPongTable) 12 | const pongs = pings.map(price => pingPongTable[price]) 13 | const orders = [] 14 | 15 | pings.forEach(price => { 16 | orders.push(new Order({ 17 | symbol, 18 | price, 19 | cid: nonce(), 20 | type: _margin ? Order.type.LIMIT : Order.type.EXCHANGE_LIMIT, 21 | amount, 22 | hidden, 23 | })) 24 | }) 25 | 26 | orders.push({ label: 'PONGS FOLLOW' }) 27 | 28 | pongs.forEach(price => { 29 | orders.push(new Order({ 30 | symbol, 31 | price, 32 | cid: nonce(), 33 | type: _margin ? Order.type.LIMIT : Order.type.EXCHANGE_LIMIT, 34 | amount: -amount, 35 | hidden, 36 | })) 37 | }) 38 | 39 | if (endless) { 40 | orders.push({ label: 'REPEATS ENDLESSLY' }) 41 | } 42 | 43 | return orders 44 | } 45 | -------------------------------------------------------------------------------- /lib/ping_pong/meta/get_ui_def.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = () => ({ 4 | label: 'Ping/Pong', 5 | customHelp: 'Ping/pong submits multiple \'ping\' orders; once a ping order fills, an associated \'pong\' order is submitted.\n\nMultiple ping/pong pairs can be created by specifying an order count greater than 1, a suitable min/max ping price, and a pong distance. Multiple ping orders will be created between the specified min/max prices, with the associated pongs offset by the pong distance from the ping price', 6 | connectionTimeout: 10000, 7 | actionTimeout: 10000, 8 | 9 | header: { 10 | component: 'ui.checkbox_group', 11 | fields: ['hidden', 'endless'], 12 | }, 13 | 14 | sections: [{ 15 | title: '', 16 | name: 'general', 17 | rows: [ 18 | ['action', null], 19 | ['amount', 'orderCount'] 20 | ] 21 | }, { 22 | title: '', 23 | name: 'single_ping', 24 | rows: [ 25 | ['pingPrice', 'pongPrice'] 26 | ], 27 | 28 | visible: { 29 | orderCount: { eq: '1' } 30 | } 31 | }, { 32 | title: '', 33 | name: 'multi_ping', 34 | rows: [ 35 | ['pingMinPrice', 'pongDistance'], 36 | ['pingMaxPrice', null] 37 | ], 38 | 39 | visible: { 40 | orderCount: { gt: 1 } 41 | } 42 | }], 43 | 44 | fields: { 45 | hidden: { 46 | component: 'input.checkbox', 47 | label: 'HIDDEN', 48 | default: false, 49 | help: 'trading.hideorder_tooltip', 50 | }, 51 | 52 | endless: { 53 | component: 'input.checkbox', 54 | label: 'ENDLESS', 55 | default: false, 56 | customHelp: 'If true, pings will be recreated once their associated pongs fill' 57 | }, 58 | 59 | pingPrice: { 60 | component: 'input.price', 61 | label: 'Ping Price $QUOTE', 62 | }, 63 | 64 | pongPrice: { 65 | component: 'input.price', 66 | label: 'Pong Price $QUOTE', 67 | }, 68 | 69 | pongDistance: { 70 | component: 'input.number', 71 | label: 'Pong Distance', 72 | }, 73 | 74 | pingMinPrice: { 75 | component: 'input.price', 76 | label: 'Ping Min Price $QUOTE', 77 | }, 78 | 79 | pingMaxPrice: { 80 | component: 'input.price', 81 | label: 'Ping Max Price $QUOTE', 82 | }, 83 | 84 | amount: { 85 | component: 'input.amount', 86 | label: 'Amount $BASE', 87 | customHelp: 'Total order amount' 88 | }, 89 | 90 | orderCount: { 91 | component: 'input.number', 92 | label: 'Order Count', 93 | default: '1' 94 | }, 95 | 96 | action: { 97 | component: 'input.radio', 98 | label: 'Action', 99 | options: ['Buy', 'Sell'], 100 | inline: true, 101 | default: 'Buy' 102 | } 103 | }, 104 | 105 | actions: ['preview', 'submit'] 106 | }) 107 | -------------------------------------------------------------------------------- /lib/ping_pong/meta/init_state.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const genPingPongTable = require('../util/gen_ping_pong_table') 4 | const { BollingerBands } = require('bfx-hf-indicators') 5 | 6 | module.exports = (args = {}) => { 7 | const { bbandsPeriod, bbandsMul } = args 8 | const pingPongTable = genPingPongTable(args) 9 | const bbands = new BollingerBands([bbandsPeriod, bbandsMul]) 10 | 11 | return { 12 | bbands, 13 | activePongs: {}, // reverse mapping of pingPongTable 14 | pingPongTable, 15 | args 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/ping_pong/meta/process_params.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = (data) => { 4 | const params = { ...data } 5 | 6 | if (params._symbol) { 7 | params.symbol = params._symbol 8 | delete params._symbol 9 | } 10 | 11 | if (!params.cancelDelay) { 12 | params.cancelDelay = 1000 13 | } 14 | 15 | if (!params.submitDelay) { 16 | params.submitDelay = 2000 17 | } 18 | 19 | if (params.action) { 20 | if (params.action === 'Sell') { 21 | params.amount = Number(params.amount) * -1 22 | } 23 | 24 | delete params.action 25 | } 26 | 27 | return params 28 | } 29 | -------------------------------------------------------------------------------- /lib/ping_pong/meta/serialize.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = (state = {}) => { 4 | const { 5 | bbands, follow, pingPongTable, activePongs, args = {} 6 | } = state 7 | 8 | return { 9 | // bbands: bbands.serialize(), 10 | pingPongTable, 11 | activePongs, 12 | follow, 13 | args, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/ping_pong/meta/unserialize.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { BollingerBands } = require('bfx-hf-indicators') 4 | 5 | module.exports = (loadedState = {}) => { 6 | const { 7 | bbands, follow, pingPongTable, activePongs, args = {} 8 | } = loadedState 9 | 10 | return { 11 | // bbands: BollingerBands.unserialize(bbands), 12 | pingPongTable, 13 | activePongs, 14 | follow, 15 | args, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/ping_pong/meta/validate_params.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const _isFinite = require('lodash/isFinite') 4 | 5 | module.exports = (args = {}) => { 6 | const { 7 | amount, pingPrice, pongPrice, pingMinPrice, pingMaxPrice, orderCount, 8 | pongDistance, 9 | } = args 10 | 11 | if (!_isFinite(orderCount) || orderCount < 1) { 12 | return `Invalid order count: ${orderCount}` 13 | } 14 | 15 | if (!_isFinite(amount)) return 'Invalid amount' 16 | if (amount > 0 && pongPrice < pingPrice && orderCount === 1) { 17 | return 'Pong price must be greater than ping price for buy orders' 18 | } else if (amount < 0 && pongPrice > pingPrice && orderCount === 1) { 19 | return 'Pong price must be less than ping price for sell orders' 20 | } 21 | 22 | if (orderCount > 1) { 23 | if (!_isFinite(pingMinPrice)) { 24 | return `Invalid ping min price: ${pingMinPrice}` 25 | } 26 | 27 | if (!_isFinite(pingMaxPrice)) { 28 | return `Invalid ping max price: ${pingMaxPrice}` 29 | } 30 | 31 | if (!_isFinite(pongDistance)) { 32 | return `Invalid pong distance: ${pongDistance}` 33 | } 34 | 35 | if (pingMaxPrice < pingMinPrice) { 36 | return 'Ping max price must be greater than min price' 37 | } 38 | 39 | if (pongDistance < 0) { 40 | return 'Pong distance must be positive' 41 | } 42 | } 43 | 44 | return null 45 | } 46 | -------------------------------------------------------------------------------- /lib/ping_pong/util/gen_ping_pong_table.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { preparePrice } = require('bfx-api-node-util') 4 | 5 | module.exports = (args = {}) => { 6 | const { 7 | amount, pingPrice, pongPrice, pingMinPrice, pingMaxPrice, orderCount, 8 | pongDistance, 9 | } = args 10 | 11 | const pingPongTable = {} 12 | 13 | if (orderCount === 1) { 14 | pingPongTable[preparePrice(pingPrice)] = preparePrice(pongPrice) 15 | } else { 16 | const step = (pingMaxPrice - pingMinPrice) / (orderCount - 1) 17 | 18 | for (let i = 0; i < orderCount; i += 1) { 19 | const price = pingMinPrice + (i * step) 20 | pingPongTable[preparePrice(price)] = preparePrice(amount > 0 21 | ? price + pongDistance 22 | : price - pongDistance) 23 | } 24 | } 25 | 26 | return pingPongTable 27 | } 28 | -------------------------------------------------------------------------------- /lib/testing/create_harness.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const _get = require('lodash/get') 4 | const _isFunction = require('lodash/isFunction') 5 | const AsyncEventEmitter = require('async_event_emitter') 6 | const debug = require('debug')('bfx:hf:algo:testing:harness') 7 | 8 | module.exports = (instance = {}, aoDef = {}) => { 9 | const { state = {} } = instance 10 | const ev = new AsyncEventEmitter() 11 | 12 | // Wrap AO state emitter so we can capture events 13 | state.ev._emit = state.ev.emit 14 | state.ev.emit = (eventName, ...args) => { 15 | debug('ao internal emit: %s', eventName) 16 | 17 | ev.emit(eventName, ...args) 18 | state.ev._emit(eventName, ...args) 19 | } 20 | 21 | ev.trigger = async (section, eventName, ...args) => { 22 | const sectionHandlers = (aoDef.events || {})[section] 23 | const handler = _get((sectionHandlers || {}), eventName) 24 | 25 | if (!_isFunction(handler)) { 26 | debug('no handler for event %s:%s', section, eventName) 27 | return 28 | } 29 | 30 | debug('emitting %s:%s', section, eventName) 31 | ev.emit(`${section}:${eventName}`) 32 | 33 | await handler(instance, ...args) 34 | } 35 | 36 | return ev 37 | } 38 | -------------------------------------------------------------------------------- /lib/twap/config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const config = {} 4 | const priceTargets = ['LAST', 'OB_MID', 'OB_SIDE'] 5 | const priceConditions = ['MATCH_MIDPOINT', 'MATCH_SIDE', 'MATCH_LAST'] 6 | 7 | config.PRICE_TARGET = {} 8 | config.PRICE_COND = {} 9 | 10 | priceTargets.forEach(t => { config.PRICE_TARGET[t] = t }) 11 | priceConditions.forEach(c => { config.PRICE_COND[c] = c }) 12 | 13 | module.exports = config 14 | -------------------------------------------------------------------------------- /lib/twap/events/data_managed_book.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const hasOBTarget = require('../util/has_ob_target') 4 | 5 | module.exports = async (instance = {}, book, meta) => { 6 | const { state = {}, h = {} } = instance 7 | const { args = {} } = state 8 | const { symbol } = args 9 | const { debug, updateState } = h 10 | const { chanFilter } = meta 11 | const chanSymbol = chanFilter.symbol 12 | 13 | if (!hasOBTarget(args) || symbol !== chanSymbol) { 14 | return 15 | } 16 | 17 | debug('recv updated order book for %s', symbol) 18 | 19 | await updateState(instance, { 20 | lastBook: book 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /lib/twap/events/data_trades.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const hasTradeTarget = require('../util/has_trade_target') 4 | 5 | module.exports = async (instance = {}, trades, meta) => { 6 | const { state = {}, h = {} } = instance 7 | const { args = {} } = state 8 | const { symbol } = args 9 | const { debug, updateState } = h 10 | const { chanFilter } = meta 11 | const chanSymbol = chanFilter.symbol 12 | 13 | if (!hasTradeTarget(args) || symbol !== chanSymbol) { 14 | return 15 | } 16 | 17 | const [ lastTrade ] = trades 18 | const { price } = lastTrade 19 | 20 | debug('recv last price: %f [%j]', price, lastTrade) 21 | 22 | await updateState(instance, { lastTrade }) 23 | } 24 | -------------------------------------------------------------------------------- /lib/twap/events/error_minimum_size.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * Cancel the AO if we try to submit an order below the minimum size, since it 5 | * means the remaining amount is below the min size (and therefore cannot fill) 6 | * 7 | * @param {Object} instance 8 | * @param {Order} order - order which is below the min size for its symbol 9 | */ 10 | module.exports = async (instance = {}, o) => { 11 | const { state = {}, h = {} } = instance 12 | const { gid, args = {}, orders = {} } = state 13 | const { emit, debug } = h 14 | const { cancelDelay } = args 15 | 16 | debug('received minimum size error for order: %f @ %f', o.amountOrig, o.price) 17 | debug('stopping order...') 18 | 19 | await emit('exec:order:cancel:all', gid, orders, cancelDelay) 20 | await emit('exec:stop') 21 | } 22 | -------------------------------------------------------------------------------- /lib/twap/events/insufficient_balance.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * Cancel the AO if we try to submit an order without having enough balance 5 | * 6 | * @param {Object} instance 7 | * @param {Order} order - order which is below the min size for its symbol 8 | */ 9 | module.exports = async (instance = {}, o) => { 10 | const { state = {}, h = {} } = instance 11 | const { gid, args = {}, orders = {} } = state 12 | const { emit, debug } = h 13 | const { cancelDelay } = args 14 | 15 | debug('received insufficient balance error for order: %f @ %f', o.amountOrig, o.price) 16 | debug('stopping order...') 17 | 18 | await emit('exec:order:cancel:all', gid, orders, cancelDelay) 19 | await emit('exec:stop') 20 | } 21 | -------------------------------------------------------------------------------- /lib/twap/events/life_start.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const _isFinite = require('lodash/isFinite') 4 | const _isString = require('lodash/isString') 5 | 6 | module.exports = async (instance = {}) => { 7 | const { state = {}, h = {} } = instance 8 | const { args = {} } = state 9 | const { sliceInterval, priceTarget, priceCondition, orderType } = args 10 | const { debug, emitSelf, updateState } = h 11 | 12 | if (!/MARKET/.test(orderType)) { 13 | if (_isFinite(priceTarget) && _isString(priceCondition)) { 14 | debug('running in condition monitoring mode (%s = %f)', priceCondition, priceTarget) 15 | } else if (_isString(priceTarget)) { 16 | debug('running in soft match mode (%s)', priceTarget) 17 | } else { 18 | debug('can\'t start, invalid operating mode (target %s, condition %s)', priceTarget, priceCondition) 19 | return 20 | } 21 | } 22 | 23 | const interval = setInterval(async () => { 24 | await emitSelf('interval_tick') 25 | }, sliceInterval) 26 | 27 | debug('scheduled interval (%f s)', sliceInterval / 1000) 28 | 29 | await updateState(instance, { interval }) 30 | } 31 | -------------------------------------------------------------------------------- /lib/twap/events/life_stop.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = async (instance = {}) => { 4 | const { state = {}, h = {} } = instance 5 | const { interval } = state 6 | const { debug, updateState } = h 7 | 8 | if (interval !== null) { 9 | clearInterval(interval) 10 | updateState(instance, { interval: null }) 11 | debug('cleared interval') 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/twap/events/orders_order_cancel.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = async (instance = {}, order) => { 4 | const { state = {}, h = {} } = instance 5 | const { args = {}, orders = {}, gid } = state 6 | const { emit, debug } = h 7 | const { cancelDelay } = args 8 | 9 | debug('detected atomic cancelation, stopping...') 10 | 11 | await emit('exec:order:cancel:all', gid, orders, cancelDelay) 12 | await emit('exec:stop') 13 | } 14 | -------------------------------------------------------------------------------- /lib/twap/events/orders_order_fill.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { Config } = require('bfx-api-node-core') 4 | const { DUST } = Config 5 | 6 | module.exports = async (instance = {}, order) => { 7 | const { state = {}, h = {} } = instance 8 | const { args = {} } = state 9 | const { emit, updateState, debug } = h 10 | const { amount } = args 11 | const m = amount < 0 ? -1 : 1 12 | 13 | const fillAmount = order.getLastFillAmount() 14 | const remainingAmount = state.remainingAmount - fillAmount 15 | const absRem = m < 0 ? remainingAmount * -1 : remainingAmount 16 | 17 | order.resetFilledAmount() 18 | 19 | debug('updated remaining amount: %f [filled %f]', remainingAmount, fillAmount) 20 | 21 | await updateState(instance, { remainingAmount }) 22 | 23 | if (absRem > DUST) { // continue, await next tick 24 | return 25 | } 26 | 27 | if (absRem < 0) { 28 | debug('warning: overfill! %f', absRem) 29 | } 30 | 31 | await emit('exec:stop') 32 | } 33 | -------------------------------------------------------------------------------- /lib/twap/events/self_interval_tick.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const _isEmpty = require('lodash/isEmpty') 4 | const _isFinite = require('lodash/isFinite') 5 | const hasTradeTarget = require('../util/has_trade_target') 6 | const hasOBTarget = require('../util/has_ob_target') 7 | const generateOrder = require('../util/generate_order') 8 | const getOBPrice = require('../util/get_ob_price') 9 | const getTradePrice = require('../util/get_trade_price') 10 | const isTargetMet = require('../util/is_target_met') 11 | 12 | module.exports = async (instance = {}) => { 13 | const { state = {}, h = {} } = instance 14 | const { orders = {}, args = {}, gid } = state 15 | const { emit, debug } = h 16 | const { 17 | priceTarget, tradeBeyondEnd, cancelDelay, submitDelay, priceDelta, 18 | orderType, sliceAmount, amount 19 | } = args 20 | 21 | debug('tick') 22 | 23 | if (!tradeBeyondEnd && !_isEmpty(orders)) { 24 | await emit('exec:order:cancel:all', gid, orders, cancelDelay) 25 | } 26 | 27 | // Ensure that the next order would not push us over the total amount 28 | if (tradeBeyondEnd) { 29 | let openAmount = 0 30 | 31 | Object.values(orders).forEach(o => { openAmount += o.amount }) 32 | 33 | if (openAmount + sliceAmount > amount) { 34 | debug('next tick would exceed total order amount, refusing') 35 | return 36 | } 37 | } 38 | 39 | let orderPrice 40 | 41 | if (!/MARKET/.test(orderType)) { 42 | if (hasTradeTarget(args)) { 43 | orderPrice = getTradePrice(state) 44 | } else if (hasOBTarget(args)) { 45 | orderPrice = getOBPrice(state) 46 | } 47 | 48 | if (!_isFinite(orderPrice)) { 49 | debug('price data unavailable, awaiting next tick') 50 | return 51 | } 52 | 53 | if (_isFinite(priceTarget)) { 54 | const targetMet = isTargetMet(args, orderPrice) 55 | 56 | if (!targetMet) { 57 | debug('target not met | price %f (target %s delta %f)', orderPrice, priceTarget, priceDelta) 58 | return 59 | } 60 | } 61 | 62 | debug('target met | price %f (target %s)', orderPrice, priceTarget) 63 | } 64 | 65 | const order = generateOrder(state, orderPrice) 66 | 67 | if (order) { 68 | await emit('exec:order:submit:all', gid, [order], submitDelay) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/twap/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const defineAlgoOrder = require('../define_algo_order') 4 | 5 | const validateParams = require('./meta/validate_params') 6 | const processParams = require('./meta/process_params') 7 | const initState = require('./meta/init_state') 8 | const onSelfIntervalTick = require('./events/self_interval_tick') 9 | const onLifeStart = require('./events/life_start') 10 | const onLifeStop = require('./events/life_stop') 11 | const onOrdersOrderFill = require('./events/orders_order_fill') 12 | const onOrdersOrderCancel = require('./events/orders_order_cancel') 13 | const onDataManagedBook = require('./events/data_managed_book') 14 | const onInsufficientBalanceError = require('./events/insufficient_balance') 15 | const onMinimumSizeError = require('./events/error_minimum_size') 16 | const onDataTrades = require('./events/data_trades') 17 | const genOrderLabel = require('./meta/gen_order_label') 18 | const getUIDef = require('./meta/get_ui_def') 19 | const genPreview = require('./meta/gen_preview') 20 | const declareEvents = require('./meta/declare_events') 21 | const declareChannels = require('./meta/declare_channels') 22 | const serialize = require('./meta/serialize') 23 | const unserialize = require('./meta/unserialize') 24 | const config = require('./config') 25 | 26 | /** 27 | * TWAP spreads an order out through time in order to fill at the time-weighted 28 | * average price, calculated between the time the order is submitted to the 29 | * final atomic order close. 30 | * 31 | * The price can be specified as a fixed external target, such as the top 32 | * bid/ask or last trade price, or as an explicit target which must be matched 33 | * against the top bid/ask/last trade/etc. 34 | * 35 | * Available price targets/explicit target conditions: 36 | * * OB side price (top bid/ask) 37 | * * OB mid price 38 | * * Last trade price 39 | * 40 | * @name TWAP 41 | * @param {string} symbol - symbol to trade on 42 | * @param {number} amount - total order amount 43 | * @param {number} sliceAmount - individual slice order amount 44 | * @param {number} priceDelta - max acceptable distance from price target 45 | * @param {string?} priceCondition - MATCH_LAST, MATCH_SIDE, MATCH_MID 46 | * @param {number|string} priceTarget - numeric, or OB_SIDE, OB_MID, LAST 47 | * @param {boolean} tradeBeyondEnd - if true, slices are not cancelled after their interval expires 48 | * @param {string} orderType - LIMIT or MARKET 49 | * @param {boolean} _margin - if false, order type is prefixed with EXCHANGE 50 | * @param {number?} submitDelay - in ms, defaults to 1500 51 | * @param {number?} cancelDelay - in ms, defaults to 5000 52 | */ 53 | const TWAP = defineAlgoOrder({ 54 | id: 'bfx.twap', 55 | name: 'TWAP', 56 | 57 | meta: { 58 | validateParams, 59 | processParams, 60 | declareEvents, 61 | declareChannels, 62 | genOrderLabel, 63 | genPreview, 64 | initState, 65 | getUIDef, 66 | serialize, 67 | unserialize 68 | }, 69 | 70 | events: { 71 | self: { 72 | interval_tick: onSelfIntervalTick 73 | }, 74 | 75 | life: { 76 | start: onLifeStart, 77 | stop: onLifeStop 78 | }, 79 | 80 | orders: { 81 | order_fill: onOrdersOrderFill, 82 | order_cancel: onOrdersOrderCancel 83 | }, 84 | 85 | data: { 86 | managedBook: onDataManagedBook, 87 | trades: onDataTrades 88 | }, 89 | 90 | errors: { 91 | minimum_size: onMinimumSizeError, 92 | insufficient_balance: onInsufficientBalanceError 93 | } 94 | } 95 | }) 96 | 97 | TWAP.Config = config 98 | 99 | module.exports = TWAP 100 | -------------------------------------------------------------------------------- /lib/twap/meta/declare_channels.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const hasTradeTarget = require('../util/has_trade_target') 4 | const hasOBTarget = require('../util/has_ob_target') 5 | 6 | module.exports = async (instance = {}, host) => { 7 | const { h = {}, state = {} } = instance 8 | const { args = {} } = state 9 | const { symbol, priceTarget } = args 10 | const { declareChannel } = h 11 | 12 | if (hasTradeTarget(args)) { 13 | await declareChannel(instance, host, 'trades', { symbol }) 14 | } else if (hasOBTarget(args)) { 15 | await declareChannel(instance, host, 'book', { 16 | symbol, 17 | prec: 'R0', 18 | len: '25' 19 | }) 20 | } else { 21 | throw new Error(`invalid price target ${priceTarget}`) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/twap/meta/declare_events.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = (instance = {}, host) => { 4 | const { h = {} } = instance 5 | const { declareEvent } = h 6 | 7 | declareEvent(instance, host, 'self:interval_tick', 'interval_tick') 8 | } 9 | -------------------------------------------------------------------------------- /lib/twap/meta/gen_order_label.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = (state = {}) => { 4 | const { args = {} } = state 5 | const { 6 | sliceAmount, sliceInterval, amount, priceTarget, priceCondition, 7 | tradeBeyondEnd 8 | } = args 9 | 10 | return [ 11 | 'TWAP', 12 | ' | slice ', sliceAmount, 13 | ' | total ', amount, 14 | ' | interval ', Math.floor(sliceInterval / 1000), 's', 15 | ' | target ', priceTarget, 16 | ' | target == ', priceCondition, 17 | ' | TBE ', tradeBeyondEnd 18 | ].join('') 19 | } 20 | -------------------------------------------------------------------------------- /lib/twap/meta/gen_preview.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const generateOrder = require('../util/generate_order') 4 | 5 | module.exports = (args = {}) => { 6 | const { sliceInterval, amount } = args 7 | const m = amount < 0 ? -1 : 1 8 | const startTS = Date.now() 9 | const orders = [] 10 | 11 | let remAmount = Math.abs(amount) 12 | let o 13 | let ts = Date.now() 14 | let delay 15 | 16 | while (remAmount > 0) { 17 | o = generateOrder({ 18 | remainingAmount: remAmount * m, 19 | args 20 | }, 'PRICE') 21 | 22 | if (o === null) { 23 | break 24 | } 25 | 26 | o.amount = Math.min(Math.abs(o.amount), remAmount) * m 27 | remAmount -= Math.abs(o.amount) 28 | 29 | orders.push(o) 30 | 31 | // Convert duration to timeout 32 | let currIntervalPos = ((ts - startTS) / sliceInterval) 33 | currIntervalPos -= Math.floor(currIntervalPos) 34 | 35 | delay = (sliceInterval * (1 - currIntervalPos)) + 100 36 | 37 | if (remAmount > 0) { 38 | orders.push({ 39 | label: `DELAY ${Math.floor(delay / 1000)}s` 40 | }) 41 | } 42 | 43 | ts += delay 44 | } 45 | 46 | return orders 47 | } 48 | -------------------------------------------------------------------------------- /lib/twap/meta/get_ui_def.js: -------------------------------------------------------------------------------- 1 | module.exports = () => ({ 2 | label: 'TWAP', 3 | customHelp: 'TWAP spreads an order out through time in order to fill at the time-weighted average price, calculated between the time the order is submitted to the final atomic order close.\n\nThe price target may be set either by the order book, last trade price, or a custom explicit target that must be conditionally matched against another factor.\n\nWith custom price targets, the price condition may be specified to match against either the order book or last trade. If a price delta is specified, the target must be within the delta in order to match.', 4 | connectionTimeout: 10000, 5 | actionTimeout: 10000, 6 | 7 | header: { 8 | component: 'ui.checkbox_group', 9 | fields: ['tradeBeyondEnd'] 10 | }, 11 | 12 | sections: [{ 13 | title: '', 14 | name: 'general', 15 | rows: [ 16 | ['orderType', 'amount'], 17 | ['sliceAmount', 'sliceInterval'], 18 | ['submitDelaySec', 'cancelDelaySec'] 19 | ] 20 | }, { 21 | title: '', 22 | name: 'price', 23 | 24 | visible: { 25 | orderType: { eq: 'LIMIT' } 26 | }, 27 | 28 | rows: [ 29 | ['priceTarget', 'price'], 30 | ['priceCondition', 'priceDelta'] 31 | ] 32 | }, { 33 | rows: [['action', null]] 34 | }], 35 | 36 | fields: { 37 | submitDelaySec: { 38 | component: 'input.number', 39 | label: 'Submit Delay (sec)', 40 | customHelp: 'Seconds to wait before submitting orders', 41 | default: 2 42 | }, 43 | 44 | cancelDelaySec: { 45 | component: 'input.number', 46 | label: 'Cancel Delay (sec)', 47 | customHelp: 'Seconds to wait before cancelling orders', 48 | default: 1 49 | }, 50 | 51 | tradeBeyondEnd: { 52 | component: 'input.checkbox', 53 | label: 'Trade Beyond End', 54 | customHelp: 'Continue trading beyond slice interval', 55 | default: false 56 | }, 57 | 58 | orderType: { 59 | component: 'input.dropdown', 60 | label: 'Order Type', 61 | default: 'LIMIT', 62 | options: { 63 | LIMIT: 'Limit', 64 | MARKET: 'Market' 65 | } 66 | }, 67 | 68 | amount: { 69 | component: 'input.amount', 70 | label: 'Amount $BASE', 71 | customHelp: 'Total order amount', 72 | priceField: 'price' 73 | }, 74 | 75 | sliceAmount: { 76 | component: 'input.number', 77 | label: 'Slice Amount $BASE', 78 | customHelp: 'Total slice size' 79 | }, 80 | 81 | sliceInterval: { 82 | component: 'input.number', 83 | label: 'Slice Interval (sec)', 84 | customHelp: 'Duration over which to trade slice' 85 | }, 86 | 87 | priceDelta: { 88 | component: 'input.number', 89 | label: 'Target Delta', 90 | customHelp: '± Distance from price target for match', 91 | disabled: { 92 | priceTarget: { neq: 'CUSTOM' } 93 | } 94 | }, 95 | 96 | price: { 97 | component: 'input.price', 98 | label: 'Price $QUOTE', 99 | customHelp: 'Requires \'custom\' price target', 100 | disabled: { 101 | priceTarget: { neq: 'CUSTOM' } 102 | } 103 | }, 104 | 105 | priceTarget: { 106 | component: 'input.dropdown', 107 | label: 'Price Target', 108 | default: 'OB_MID', 109 | options: { 110 | OB_MID: 'OB mid price', 111 | OB_SIDE: 'OB side price', 112 | LAST: 'Last trade price', 113 | CUSTOM: 'Custom' 114 | } 115 | }, 116 | 117 | priceCondition: { 118 | component: 'input.dropdown', 119 | label: 'Price Condition', 120 | default: 'MATCH_MIDPOINT', 121 | customHelp: 'Match point for custom price targets', 122 | visible: { 123 | priceTarget: { eq: 'CUSTOM' } 124 | }, 125 | 126 | options: { 127 | MATCH_MIDPOINT: 'Match OB mid price', 128 | MATCH_SIDE: 'Match OB side', 129 | MATCH_LAST: 'Match last trade price' 130 | } 131 | }, 132 | 133 | action: { 134 | component: 'input.radio', 135 | label: 'Action', 136 | options: ['Buy', 'Sell'], 137 | inline: true, 138 | default: 'Buy' 139 | } 140 | }, 141 | 142 | actions: ['preview', 'submit'] 143 | }) 144 | -------------------------------------------------------------------------------- /lib/twap/meta/init_state.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = (args = {}) => { 4 | const { amount } = args 5 | 6 | return { 7 | interval: null, 8 | remainingAmount: amount, 9 | args 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/twap/meta/process_params.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const _isFinite = require('lodash/isFinite') 4 | 5 | module.exports = (data) => { 6 | const params = { ...data } 7 | 8 | if (params.orderType && !params._margin) { 9 | params.orderType = `EXCHANGE ${params.orderType}` 10 | } 11 | 12 | if (params._symbol) { 13 | params.symbol = params._symbol 14 | delete params._symbol 15 | } 16 | 17 | if (params.cancelDelaySec) { 18 | params.cancelDelay = params.cancelDelaySec * 1000 19 | delete params.cancelDelaySec 20 | } 21 | 22 | if (params.submitDelaySec) { 23 | params.submitDelay = params.submitDelaySec * 1000 24 | delete params.submitDelaySec 25 | } 26 | 27 | if (!params.cancelDelay) { 28 | params.cancelDelay = 1000 29 | } 30 | 31 | if (!params.submitDelay) { 32 | params.submitDelay = 2000 33 | } 34 | 35 | if (params.priceTarget === 'CUSTOM') { 36 | params.priceTarget = params.price 37 | } 38 | 39 | if (_isFinite(params.priceDelta)) { 40 | params.priceDelta = Math.abs(params.priceDelta) 41 | } else { 42 | params.priceDelta = 0 43 | } 44 | 45 | if (_isFinite(params.sliceInterval)) { 46 | params.sliceInterval = Number(params.sliceInterval) * 1000 47 | } 48 | 49 | delete params.price 50 | 51 | if (params.action) { 52 | if (params.action === 'Sell') { 53 | params.amount = Number(params.amount) * -1 54 | params.sliceAmount = Number(params.sliceAmount) * -1 55 | } 56 | 57 | delete params.action 58 | } 59 | 60 | return params 61 | } 62 | -------------------------------------------------------------------------------- /lib/twap/meta/serialize.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = (state = {}) => { 4 | const { remainingAmount, args = {} } = state 5 | 6 | return { 7 | remainingAmount, 8 | args 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/twap/meta/unserialize.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = (loadedState = {}) => { 4 | const { remainingAmount, args = {} } = loadedState 5 | 6 | return { 7 | remainingAmount, 8 | args 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/twap/meta/validate_params.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { Order } = require('bfx-api-node-models') 4 | const _isFinite = require('lodash/isFinite') 5 | const _isString = require('lodash/isString') 6 | const _isUndefined = require('lodash/isUndefined') 7 | const Config = require('../config') 8 | 9 | module.exports = (args = {}) => { 10 | const { 11 | orderType, amount, sliceAmount, sliceInterval, priceTarget, priceCondition, 12 | cancelDelay, submitDelay, priceDelta 13 | } = args 14 | 15 | let err = null 16 | 17 | if (!Order.type[orderType]) err = 'invalid order type' 18 | if (!_isFinite(cancelDelay) || cancelDelay < 0) err = 'invalid cancel delay' 19 | if (!_isFinite(submitDelay) || submitDelay < 0) err = 'invalid submit delay' 20 | if (!_isFinite(amount)) err = 'invalid amount' 21 | if (!_isFinite(sliceAmount)) err = 'invalid slice amount' 22 | if (!_isFinite(sliceInterval)) err = 'slice interval not a number' 23 | if (sliceInterval <= 0) err = 'slice interval <= 0' 24 | if (!_isString(priceTarget) && !_isFinite(priceTarget)) { 25 | err = 'invalid price target' 26 | } else if (_isFinite(priceTarget) && priceTarget <= 0) { 27 | err = 'negative custom price target' 28 | } else if (_isFinite(priceTarget) && !Config.PRICE_COND[priceCondition]) { 29 | err = 'invalid condition for custom price target' 30 | } else if (_isString(priceTarget) && !Config.PRICE_TARGET[priceTarget]) { 31 | err = 'invalid matched price target' 32 | } else if (!_isUndefined(priceDelta) && !_isFinite(priceDelta)) { 33 | err = 'invalid price delta provided' 34 | } 35 | 36 | if ( 37 | (amount < 0 && sliceAmount >= 0) || 38 | (amount > 0 && sliceAmount <= 0) 39 | ) { 40 | return 'Amount & slice amount must have same sign' 41 | } 42 | 43 | return err 44 | } 45 | -------------------------------------------------------------------------------- /lib/twap/util/generate_order.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const _isString = require('lodash/isString') 4 | const { Order } = require('bfx-api-node-models') 5 | const { nonce } = require('bfx-api-node-util') 6 | const { Config } = require('bfx-api-node-core') 7 | const { DUST } = Config 8 | 9 | module.exports = (state = {}, price) => { 10 | const { args = {}, remainingAmount } = state 11 | const { sliceAmount, orderType, symbol, amount, _margin } = args 12 | 13 | const m = amount < 0 ? -1 : 1 14 | const rem = m === 1 15 | ? Math.min(sliceAmount, remainingAmount) 16 | : Math.max(sliceAmount, remainingAmount) 17 | 18 | if (Math.abs(rem) < DUST) { 19 | return null 20 | } 21 | 22 | return new Order({ 23 | symbol, 24 | price, 25 | cid: nonce(), 26 | amount: rem, 27 | type: _isString(orderType) 28 | ? orderType 29 | : _margin 30 | ? 'MARKET' 31 | : 'EXCHANGE MARKET' 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /lib/twap/util/get_ob_price.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const _isFinite = require('lodash/isFinite') 4 | const Config = require('../config') 5 | 6 | /** 7 | * Extracts the target price from the last known order book. Null if unavailable 8 | * 9 | * @param {Object} state 10 | * @return {number} obPrice 11 | */ 12 | module.exports = (state = {}) => { 13 | const { args = {}, lastBook } = state 14 | const { amount, priceTarget, priceCondition } = args 15 | let price = null 16 | 17 | if (!lastBook) { 18 | return null 19 | } 20 | 21 | if (_isFinite(priceTarget)) { 22 | if (priceCondition === Config.PRICE_COND.MATCH_SIDE) { 23 | price = amount < 0 24 | ? lastBook.topBid() 25 | : lastBook.topAsk() 26 | } else if (priceCondition === Config.PRICE_COND.MATCH_MIDPOINT) { 27 | price = lastBook.midPrice() 28 | } 29 | } else if (priceTarget === Config.PRICE_TARGET.OB_SIDE) { 30 | price = amount < 0 31 | ? lastBook.topBid() 32 | : lastBook.topAsk() 33 | } else if (priceTarget === Config.PRICE_TARGET.OB_MID) { 34 | price = lastBook.midPrice() 35 | } 36 | 37 | return price 38 | } 39 | -------------------------------------------------------------------------------- /lib/twap/util/get_trade_price.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const _isFinite = require('lodash/isFinite') 4 | const Config = require('../config') 5 | 6 | /** 7 | * Extracts the target price from the last known trade. Null if unavailable 8 | * 9 | * @param {Object} state 10 | * @return {number} obPrice 11 | */ 12 | module.exports = (state = {}) => { 13 | const { args = {}, lastTrade } = state 14 | const { priceTarget, priceCondition } = args 15 | 16 | if (!lastTrade) { 17 | return null 18 | } 19 | 20 | const { price } = lastTrade 21 | 22 | if (_isFinite(priceTarget)) { 23 | if (priceCondition === Config.PRICE_COND.MATCH_LAST) { 24 | return price 25 | } 26 | } else if (priceTarget === Config.PRICE_TARGET.LAST) { 27 | return price 28 | } 29 | 30 | return null 31 | } 32 | -------------------------------------------------------------------------------- /lib/twap/util/has_ob_target.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const _isString = require('lodash/isString') 4 | const _isFinite = require('lodash/isFinite') 5 | const Config = require('../config') 6 | 7 | module.exports = (args = {}) => { 8 | const { priceTarget, priceCondition } = args 9 | 10 | if (_isFinite(priceTarget) && ( // explicit book match 11 | (priceCondition === Config.PRICE_COND.MATCH_SIDE) || 12 | (priceCondition === Config.PRICE_COND.MATCH_MIDPOINT) 13 | )) { 14 | return true 15 | } 16 | 17 | if (_isString(priceTarget) && ( // soft book match 18 | (priceTarget === Config.PRICE_TARGET.OB_MID) || 19 | (priceTarget === Config.PRICE_TARGET.OB_SIDE) 20 | )) { 21 | return true 22 | } 23 | 24 | return false 25 | } 26 | -------------------------------------------------------------------------------- /lib/twap/util/has_trade_target.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const _isString = require('lodash/isString') 4 | const _isFinite = require('lodash/isFinite') 5 | const Config = require('../config') 6 | 7 | module.exports = (args = {}) => { 8 | const { priceTarget, priceCondition } = args 9 | 10 | if (_isFinite(priceTarget) && ( // explicit trade match 11 | (priceCondition === Config.PRICE_COND.MATCH_LAST) 12 | )) { 13 | return true 14 | } 15 | 16 | if (_isString(priceTarget) && ( // soft trade match 17 | (priceTarget === Config.PRICE_TARGET.LAST) 18 | )) { 19 | return true 20 | } 21 | 22 | return false 23 | } 24 | -------------------------------------------------------------------------------- /lib/twap/util/is_target_met.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const _isFinite = require('lodash/isFinite') 4 | 5 | module.exports = (args = {}, price) => { 6 | const { priceTarget, priceDelta } = args 7 | 8 | return _isFinite(priceDelta) 9 | ? price > (priceTarget - priceDelta) && price < (priceTarget + priceDelta) 10 | : price === priceTarget 11 | } 12 | -------------------------------------------------------------------------------- /lib/util/has_open_orders.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = (orders = {}) => { 4 | return !!Object.values(orders).find((order = {}) => { 5 | const { status } = order 6 | 7 | return !status || ( 8 | !status.match(/CANCELED/) && !status.match(/EXECUTED/) 9 | ) 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bfx-hf-algo", 3 | "version": "1.0.6", 4 | "description": "HF Algorithmic Order Module", 5 | "main": "index.js", 6 | "directories": { 7 | "lib": "lib" 8 | }, 9 | "engines": { 10 | "node": ">=6" 11 | }, 12 | "author": "Cris Mihalache (https://www.bitfinex.com)", 13 | "license": "Apache-2.0", 14 | "scripts": { 15 | "lint": "standard", 16 | "test": "npm run lint && npm run unit", 17 | "unit": "DEBUG='bfx:hf:*' NODE_PATH=lib/ NODE_ENV=test istanbul cover _mocha -- -R spec -b --recursive", 18 | "test-without-coverage": "NODE_ENV=test mocha -R spec -b --recursive", 19 | "aohost_docs": "node_modules/jsdoc-to-markdown/bin/cli.js lib/ao_host.js > docs/ao_host.md", 20 | "helpers_docs": "node_modules/jsdoc-to-markdown/bin/cli.js lib/host/gen_helpers.js > docs/helpers.md", 21 | "iceberg_docs": "node_modules/jsdoc-to-markdown/bin/cli.js lib/iceberg/index.js > docs/iceberg.md", 22 | "twap_docs": "node_modules/jsdoc-to-markdown/bin/cli.js lib/twap/index.js > docs/twap.md", 23 | "ad_docs": "node_modules/jsdoc-to-markdown/bin/cli.js lib/accumulate_distribute/index.js > docs/accumulate_distribute.md", 24 | "docs": "npm run aohost_docs && npm run helpers_docs && npm run iceberg_docs && npm run twap_docs && npm run ad_docs" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/bitfinexcom/bfx-hf-algo.git" 29 | }, 30 | "bugs": { 31 | "url": "https://github.com/bitfinexcom/bfx-hf-algo/issues" 32 | }, 33 | "keywords": [ 34 | "honey framework", 35 | "bitfinex", 36 | "bitcoin", 37 | "BTC" 38 | ], 39 | "dependencies": { 40 | "bfx-api-mock-srv": "bitfinexcom/bfx-api-mock-srv", 41 | "bfx-api-node-core": "^1.0.0", 42 | "bfx-api-node-util": "^1.0.0", 43 | "bfx-api-node-models": "^1.0.0", 44 | "bfx-api-node-plugin-managed-ob": "^1.0.0", 45 | "bfx-api-node-plugin-managed-candles": "^1.0.0", 46 | "bfx-api-node-plugin-wd": "^1.0.0", 47 | "bitfinex-api-node": "^2.0.0", 48 | "bfx-hf-indicators": "^1.0.0", 49 | "bfx-hf-util": "^1.0.0", 50 | "bfx-hf-models": "^1.0.0", 51 | "bluebird": "^3.5.1", 52 | "eventemitter2": "^5.0.1", 53 | "lodash": "^4.17.10", 54 | "lodash.throttle": "^4.1.1", 55 | "p-iteration": "^1.1.7" 56 | }, 57 | "devDependencies": { 58 | "babel-eslint": "^8.2.6", 59 | "dotenv": "^6.0.0", 60 | "eslint": "^4.19.1", 61 | "eslint-config-airbnb": "^16.1.0", 62 | "eslint-config-standard": "^7.0.0", 63 | "eslint-loader": "^1.7.1", 64 | "eslint-plugin-import": "^2.12.0", 65 | "eslint-plugin-jsx-a11y": "^6.0.3", 66 | "eslint-plugin-promise": "^3.5.0", 67 | "eslint-plugin-standard": "^2.0.1", 68 | "faucet": "^0.0.1", 69 | "istanbul": "^1.1.0-alpha.1", 70 | "jsdoc-to-markdown": "^4.0.1", 71 | "mocha": "^5.2.0", 72 | "socks-proxy-agent": "^4.0.1", 73 | "tape": "^4.6.3", 74 | "tape-watch": "^2.3.0", 75 | "webpack": "^2.4.1", 76 | "yargs": "6.6.0" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /test/iceberg/events/life_start.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict' 3 | 4 | const assert = require('assert') 5 | const Promise = require('bluebird') 6 | const onLifeStart = require('iceberg/events/life_start') 7 | 8 | describe('iceberg:events:life_start', () => { 9 | it('submits orders on startup', (done) => { 10 | onLifeStart({ h: { 11 | emitSelf: (eName) => { 12 | return new Promise((resolve) => { 13 | assert.equal(eName, 'submit_orders') 14 | resolve() 15 | }).then(done).catch(done) 16 | } 17 | }}) 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /test/iceberg/events/life_stop.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict' 3 | 4 | const assert = require('assert') 5 | const Promise = require('bluebird') 6 | const onLifeStop = require('iceberg/events/life_stop') 7 | 8 | describe('iceberg:events:life_stop', () => { 9 | it('submits all known orders for cancellation', (done) => { 10 | const orderState = { 11 | 1: 'some_order_object' 12 | } 13 | 14 | onLifeStop({ 15 | state: { 16 | gid: 100, 17 | args: { cancelDelay: 42 }, 18 | orders: orderState 19 | }, 20 | 21 | h: { 22 | debouncedSubmitOrders: { 23 | cancel: () => {} 24 | }, 25 | 26 | emit: (eName, gid, orders, cancelDelay) => { 27 | return new Promise((resolve) => { 28 | assert.equal(gid, 100) 29 | assert.equal(eName, 'exec:order:cancel:all') 30 | assert.equal(cancelDelay, 42) 31 | assert.deepStrictEqual(orders, orderState) 32 | resolve() 33 | }).then(done).catch(done) 34 | } 35 | } 36 | }) 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /test/iceberg/events/orders_order_cancel.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict' 3 | 4 | const assert = require('assert') 5 | const onOrderCancel = require('iceberg/events/orders_order_cancel') 6 | 7 | describe('iceberg:events:orders_order_cancel', () => { 8 | it('submits all known orders for cancellation & stops operation', (done) => { 9 | let call = 0 10 | const orderState = { 11 | 1: 'some_order_object' 12 | } 13 | 14 | onOrderCancel({ 15 | state: { 16 | gid: 100, 17 | args: { cancelDelay: 42 }, 18 | orders: orderState 19 | }, 20 | 21 | h: { 22 | debug: () => {}, 23 | emit: async (eName, gid, orders, cancelDelay) => { 24 | if (call === 0) { 25 | assert.equal(gid, 100) 26 | assert.equal(eName, 'exec:order:cancel:all') 27 | assert.equal(cancelDelay, 42) 28 | assert.deepStrictEqual(orders, orderState) 29 | call += 1 30 | } else if (call === 1) { 31 | assert.equal(eName, 'exec:stop') 32 | done() 33 | } else { 34 | done(new Error('too many events emitted')) 35 | } 36 | } 37 | } 38 | }) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /test/iceberg/events/orders_order_fill.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict' 3 | 4 | const assert = require('assert') 5 | const Promise = require('bluebird') 6 | const onOrderFill = require('iceberg/events/orders_order_fill') 7 | 8 | describe('iceberg:events:orders_order_fill', () => { 9 | const orderState = { 1: 'some_order_object' } 10 | const instance = { 11 | state: { 12 | gid: 100, 13 | orders: orderState, 14 | args: { 15 | amount: 100, 16 | cancelDelay: 42 17 | } 18 | }, 19 | 20 | h: { 21 | debug: () => {}, 22 | updateState: async () => {}, 23 | emitSelf: async () => {}, 24 | emit: async () => {}, 25 | debouncedSubmitOrders: () => {} 26 | } 27 | } 28 | 29 | const filledOrder = { 30 | getLastFillAmount: () => { 31 | return 42 32 | } 33 | } 34 | 35 | it('cancels all known orders', (done) => { 36 | let called = 0 37 | 38 | onOrderFill({ 39 | ...instance, 40 | h: { 41 | ...instance.h, 42 | emit: (eName, gid, orders, cancelDelay) => { 43 | if (called !== 0) return 44 | called += 1 45 | 46 | return new Promise((resolve) => { 47 | assert.equal(gid, 100) 48 | assert.equal(eName, 'exec:order:cancel:all') 49 | assert.equal(cancelDelay, 42) 50 | assert.deepStrictEqual(orders, orderState) 51 | resolve() 52 | }).then(done).catch(done) 53 | } 54 | } 55 | }, filledOrder) 56 | }) 57 | 58 | it('updates remaining amount w/ fill amount', (done) => { 59 | onOrderFill({ 60 | ...instance, 61 | state: { 62 | ...instance.state, 63 | remainingAmount: 100 64 | }, 65 | 66 | h: { 67 | ...instance.h, 68 | 69 | updateState: (inst, update) => { 70 | return new Promise((resolve) => { 71 | assert.deepStrictEqual(update, { 72 | remainingAmount: 58 73 | }) 74 | resolve() 75 | }).then(done).catch(done) 76 | } 77 | } 78 | }, filledOrder) 79 | }) 80 | 81 | it('submits orders if remaining amount is not dust', (done) => { 82 | onOrderFill({ 83 | ...instance, 84 | state: { 85 | ...instance.state, 86 | remainingAmount: 100 87 | }, 88 | 89 | h: { 90 | ...instance.h, 91 | 92 | debouncedSubmitOrders: () => { 93 | done() 94 | } 95 | } 96 | }, filledOrder) 97 | }) 98 | 99 | const testStopAmount = (remainingAmount, done) => { 100 | onOrderFill({ 101 | ...instance, 102 | state: { 103 | ...instance.state, 104 | remainingAmount 105 | }, 106 | 107 | h: { 108 | ...instance.h, 109 | 110 | emitSelf: (eName) => { 111 | return new Promise((resolve) => { 112 | throw new Error('should not have submitted') 113 | }).then(done).catch(done) 114 | }, 115 | 116 | emit: (eName) => { 117 | return new Promise((resolve) => { 118 | if (eName === 'exec:stop') { 119 | done() 120 | } 121 | resolve() 122 | }).catch(done) 123 | } 124 | } 125 | }, filledOrder) 126 | } 127 | 128 | it('emits stop event if dust is left', (done) => { 129 | testStopAmount(42.00000001, done) 130 | }) 131 | 132 | it('emits stop event if no amount is left', (done) => { 133 | testStopAmount(42, done) 134 | }) 135 | }) 136 | -------------------------------------------------------------------------------- /test/iceberg/events/self_submit_orders.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict' 3 | 4 | const Promise = require('bluebird') 5 | const assert = require('assert') 6 | const onSubmitOrders = require('iceberg/events/self_submit_orders') 7 | 8 | describe('iceberg:events:self_submit_orders', () => { 9 | it('submits generated orders', (done) => { 10 | onSubmitOrders({ 11 | state: { 12 | gid: 41, 13 | remainingAmount: 0.05, 14 | args: { 15 | submitDelay: 42, 16 | excessAsHidden: false, 17 | sliceAmount: 0.1, 18 | amount: 1, 19 | price: 1000, 20 | orderType: 'EXCHANGE MARKET', 21 | symbol: 'tBTCUSD' 22 | } 23 | }, 24 | 25 | h: { 26 | emit: (eName, gid, orders, submitDelay) => { 27 | return new Promise((resolve) => { 28 | assert.equal(eName, 'exec:order:submit:all') 29 | assert.equal(gid, 41) 30 | assert.equal(submitDelay, 42) 31 | assert.equal(orders.length, 1) 32 | 33 | const [order] = orders 34 | assert.equal(order.symbol, 'tBTCUSD') 35 | assert.equal(order.type, 'EXCHANGE MARKET') 36 | assert.equal(order.price, 1000) 37 | assert.equal(order.amount, 0.05) 38 | 39 | resolve() 40 | }).then(done).catch(done) 41 | } 42 | } 43 | }) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /test/iceberg/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict' 3 | 4 | const assert = require('assert') 5 | const Iceberg = require('iceberg') 6 | const initAO = require('host/init_ao') 7 | const createTestHarness = require('testing/create_harness') 8 | 9 | const params = { 10 | symbol: 'tBTCUSD', 11 | price: 21000, 12 | amount: -0.5, 13 | sliceAmount: -0.1, 14 | excessAsHidden: true, 15 | orderType: 'LIMIT', 16 | submitDelay: 150, 17 | cancelDelay: 150, 18 | _margin: false 19 | } 20 | 21 | describe('iceberg:exec', () => { 22 | it('submits initial orders on startup', (done) => { 23 | const instance = initAO(Iceberg, params) 24 | const iTest = createTestHarness(instance, Iceberg) 25 | 26 | iTest.on('self:submit_orders', () => { 27 | done() 28 | }) 29 | 30 | iTest.trigger('life', 'start') 31 | }) 32 | 33 | it('cancels & submits new orders when an order fills', (done) => { 34 | const instance = initAO(Iceberg, params) 35 | const iTest = createTestHarness(instance, Iceberg) 36 | let cancelled = false 37 | 38 | instance.h.debouncedSubmitOrders = () => { 39 | instance.h.emitSelf('submit_orders') 40 | } 41 | 42 | iTest.on('exec:order:cancel:all', () => { 43 | cancelled = true 44 | }) 45 | 46 | iTest.on('self:submit_orders', () => { 47 | assert(cancelled) 48 | done() 49 | }) 50 | 51 | iTest.trigger('orders', 'order_fill', { 52 | getLastFillAmount: () => { return -0.05 } 53 | }) 54 | }) 55 | 56 | it('stops when remaining amount is dust or less', (done) => { 57 | const instance = initAO(Iceberg, params) 58 | const iTest = createTestHarness(instance, Iceberg) 59 | let cancelled = false 60 | let submitted = false 61 | 62 | iTest.on('exec:order:cancel:all', () => { 63 | cancelled = true 64 | }) 65 | 66 | iTest.on('self:submit_orders', () => { 67 | submitted = true 68 | }) 69 | 70 | iTest.on('exec:stop', () => { 71 | assert(cancelled) 72 | assert(!submitted) 73 | done() 74 | }) 75 | 76 | instance.state.remainingAmount = -0.05000001 77 | 78 | iTest.trigger('orders', 'order_fill', { 79 | getLastFillAmount: () => { return -0.05 } 80 | }) 81 | }) 82 | }) 83 | -------------------------------------------------------------------------------- /test/iceberg/meta/init_state.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict' 3 | 4 | const assert = require('assert') 5 | const initState = require('iceberg/meta/init_state') 6 | 7 | describe('iceberg:meta:init_state', () => { 8 | it('sets initial remainingAmount', () => { 9 | const state = initState({ 10 | amount: 42 11 | }) 12 | 13 | assert.equal(state.remainingAmount, 42) 14 | }) 15 | 16 | it('saves args on state', () => { 17 | const args = { amount: 42, otherArg: 100 } 18 | const state = initState(args) 19 | assert.deepStrictEqual(state.args, args) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /test/iceberg/meta/process_params.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict' 3 | 4 | const assert = require('assert') 5 | const _isFinite = require('lodash/isFinite') 6 | const processParams = require('iceberg/meta/process_params') 7 | 8 | describe('iceberg:meta:process_params', () => { 9 | it('adds EXCHANGE prefix for non-margin order types', () => { 10 | const exchangeParams = processParams({ orderType: 'LIMIT', _margin: false }) 11 | const marginParams = processParams({ orderType: 'LIMIT', _margin: true }) 12 | 13 | assert.equal(exchangeParams.orderType, 'EXCHANGE LIMIT') 14 | assert.equal(marginParams.orderType, 'LIMIT') 15 | }) 16 | 17 | it('integrates supplied _symbol', () => { 18 | const params = processParams({ symbol: 'tETHUSD', _symbol: 'tBTCUSD' }) 19 | assert.equal(params.symbol, 'tBTCUSD') 20 | }) 21 | 22 | it('provides defaults for cancel & submit delays', () => { 23 | const params = processParams() 24 | assert(_isFinite(params.cancelDelay)) 25 | assert(_isFinite(params.submitDelay)) 26 | }) 27 | 28 | it('negates amount if selling', () => { 29 | const buyParams = processParams({ amount: 1 }) 30 | const sellParams = processParams({ amount: 1, action: 'Sell' }) 31 | 32 | assert.equal(buyParams.amount, 1) 33 | assert.equal(sellParams.amount, -1) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /test/iceberg/meta/validate_params.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict' 3 | 4 | const assert = require('assert') 5 | const _isString = require('lodash/isString') 6 | const validateParams = require('iceberg/meta/validate_params') 7 | 8 | const validParams = { 9 | price: 1000, 10 | orderType: 'LIMIT', 11 | amount: 1, 12 | sliceAmount: 0.1, 13 | submitDelay: 100, 14 | cancelDelay: 100 15 | } 16 | 17 | describe('iceberg:meta:validate_params', () => { 18 | it('returns no error on valid params', () => { 19 | assert.equal(validateParams(validParams), null) 20 | }) 21 | 22 | it('returns error on invalid order type', () => { 23 | assert(_isString(validateParams({ 24 | ...validParams, 25 | orderType: 'nope' 26 | }))) 27 | }) 28 | 29 | it('returns error on invalid amount', () => { 30 | assert(_isString(validateParams({ 31 | ...validParams, 32 | amount: 'nope' 33 | }))) 34 | }) 35 | 36 | it('returns error on invalid slice amount', () => { 37 | assert(_isString(validateParams({ 38 | ...validParams, 39 | sliceAmount: 'nope' 40 | }))) 41 | }) 42 | 43 | it('returns error on invalid or negative submit delay', () => { 44 | assert(_isString(validateParams({ 45 | ...validParams, 46 | submitDelay: 'nope' 47 | }))) 48 | 49 | assert(_isString(validateParams({ 50 | ...validParams, 51 | submitDelay: -100 52 | }))) 53 | }) 54 | 55 | it('returns error on invalid or negative cancel delay', () => { 56 | assert(_isString(validateParams({ 57 | ...validParams, 58 | cancelDelay: 'nope' 59 | }))) 60 | 61 | assert(_isString(validateParams({ 62 | ...validParams, 63 | cancelDelay: -100 64 | }))) 65 | }) 66 | 67 | it('returns error if non-MARKET type and no price provided', () => { 68 | const params = { ...validParams } 69 | delete params.price 70 | assert(_isString(validateParams(params))) 71 | 72 | assert.equal(validateParams({ 73 | ...params, 74 | orderType: 'MARKET' 75 | }), null) 76 | }) 77 | 78 | it('returns error if amount & sliceAmount differ in sign', () => { 79 | assert(_isString(validateParams({ 80 | ...validParams, 81 | amount: -1 82 | }))) 83 | }) 84 | }) 85 | -------------------------------------------------------------------------------- /test/iceberg/util/generate_orders.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict' 3 | 4 | const assert = require('assert') 5 | const _isFinite = require('lodash/isFinite') 6 | const generateOrders = require('iceberg/util/generate_orders') 7 | const { Config } = require('bfx-api-node-core') 8 | const { DUST } = Config 9 | 10 | const args = { 11 | orderType: 'LIMIT', 12 | symbol: 'tBTCUSD', 13 | amount: 1, 14 | sliceAmount: 0.1, 15 | excessAsHidden: false, 16 | price: 1000 17 | } 18 | 19 | describe('iceberg:util:generate_orders', () => { 20 | it('generates valid slice order', () => { 21 | const orders = generateOrders({ remainingAmount: args.amount, args }) 22 | const [ slice ] = orders 23 | 24 | assert.equal(orders.length, 1) 25 | assert.equal(slice.symbol, args.symbol) 26 | assert.equal(slice.price, args.price) 27 | assert.equal(slice.type, args.orderType) 28 | assert(_isFinite(Number(slice.cid))) 29 | assert.equal(slice.amount, args.sliceAmount) 30 | }) 31 | 32 | it('caps slice order at remainingAmount if less than slice', () => { 33 | const orders = generateOrders({ remainingAmount: 0.05, args }) 34 | const [ slice ] = orders 35 | assert.equal(slice.amount, 0.05) 36 | }) 37 | 38 | it('generates hidden order if excess flag is enabled', () => { 39 | const orders = generateOrders({ 40 | remainingAmount: args.amount, 41 | args: { 42 | ...args, 43 | excessAsHidden: true 44 | } 45 | }) 46 | 47 | const [, hidden] = orders 48 | assert.equal(orders.length, 2) 49 | assert.equal(hidden.symbol, args.symbol) 50 | assert.equal(hidden.price, args.price) 51 | assert.equal(hidden.type, args.orderType) 52 | assert(_isFinite(Number(hidden.cid))) 53 | assert.equal(hidden.amount, 0.9) 54 | }) 55 | 56 | it('caps excess order at remaining amount after slice', () => { 57 | const orders = generateOrders({ 58 | remainingAmount: 0.15, 59 | args: { 60 | ...args, 61 | excessAsHidden: true 62 | } 63 | }) 64 | 65 | const [, hidden] = orders 66 | assert((hidden.amount - 0.05) < DUST) 67 | }) 68 | 69 | it('generates no hidden order if amount after slice is less than dust', () => { 70 | const orders = generateOrders({ 71 | remainingAmount: 0.10000001, 72 | args: { 73 | ...args, 74 | excessAsHidden: true 75 | } 76 | }) 77 | 78 | assert.equal(orders.length, 1) 79 | }) 80 | 81 | it('generates no orders if remaining amount is less than dust', () => { 82 | const orders = generateOrders({ 83 | remainingAmount: 0.00000001, 84 | args 85 | }) 86 | 87 | assert.equal(orders.length, 0) 88 | }) 89 | }) 90 | -------------------------------------------------------------------------------- /test/twap/events/data_managed_book.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict' 3 | 4 | const assert = require('assert') 5 | const Promise = require('bluebird') 6 | const onDataManagedBook = require('twap/events/data_managed_book') 7 | const Config = require('twap/config') 8 | 9 | const args = { 10 | symbol: 'tBTCUSD', 11 | priceTarget: 1000, 12 | priceCondition: Config.PRICE_COND.MATCH_SIDE 13 | } 14 | 15 | describe('twap:events:data_managed_book', () => { 16 | it('does nothing if price target/condition do not rely on book', (done) => { 17 | onDataManagedBook({ 18 | state: { 19 | args: { 20 | ...args, 21 | priceCondition: Config.PRICE_COND.MATCH_LAST // note trade match 22 | } 23 | }, 24 | 25 | h: { 26 | debug: () => {}, 27 | updateState: (instance, state) => { 28 | return new Promise((resolve, reject) => { 29 | reject(new Error('state should not have been updated')) 30 | }).then(done).catch(done) 31 | } 32 | } 33 | }, 42, { 34 | chanFilter: { 35 | symbol: 'tBTCUSD' 36 | } 37 | }) 38 | 39 | done() 40 | }) 41 | 42 | it('does nothing if update is for a different book', (done) => { 43 | onDataManagedBook({ 44 | state: { 45 | args 46 | }, 47 | 48 | h: { 49 | debug: () => {}, 50 | updateState: (instance, state) => { 51 | return new Promise((resolve, reject) => { 52 | reject(new Error('state should not have been updated')) 53 | }).then(done).catch(done) 54 | } 55 | } 56 | }, 42, { 57 | chanFilter: { 58 | symbol: 'tETHUSD' 59 | } 60 | }) 61 | 62 | done() 63 | }) 64 | 65 | it('updates state with provided order book if matched', (done) => { 66 | onDataManagedBook({ 67 | state: { 68 | args 69 | }, 70 | 71 | h: { 72 | debug: () => {}, 73 | updateState: (instance, state) => { 74 | return new Promise((resolve) => { 75 | assert.equal(state.lastBook, 42) 76 | resolve() 77 | }).then(done).catch(done) 78 | } 79 | } 80 | }, 42, { 81 | chanFilter: { 82 | symbol: 'tBTCUSD' 83 | } 84 | }) 85 | }) 86 | }) 87 | -------------------------------------------------------------------------------- /test/twap/events/data_trades.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict' 3 | 4 | const assert = require('assert') 5 | const Promise = require('bluebird') 6 | const onDataTrades = require('twap/events/data_trades') 7 | const Config = require('twap/config') 8 | 9 | const args = { 10 | symbol: 'tBTCUSD', 11 | priceTarget: 1000, 12 | priceCondition: Config.PRICE_COND.MATCH_LAST 13 | } 14 | 15 | describe('twap:events:data_trades', () => { 16 | it('does nothing if price target/condition do not rely trades', (done) => { 17 | onDataTrades({ 18 | state: { 19 | args: { 20 | ...args, 21 | priceCondition: Config.PRICE_COND.MATCH_SIDE // note book match 22 | } 23 | }, 24 | 25 | h: { 26 | debug: () => {}, 27 | updateState: (instance, state) => { 28 | return new Promise((resolve, reject) => { 29 | reject(new Error('state should not have been updated')) 30 | }).then(done).catch(done) 31 | } 32 | } 33 | }, [42], { 34 | chanFilter: { 35 | symbol: 'tBTCUSD' 36 | } 37 | }) 38 | 39 | done() 40 | }) 41 | 42 | it('does nothing if update is for a different symbol', (done) => { 43 | onDataTrades({ 44 | state: { 45 | args 46 | }, 47 | 48 | h: { 49 | debug: () => {}, 50 | updateState: (instance, state) => { 51 | return new Promise((resolve, reject) => { 52 | reject(new Error('state should not have been updated')) 53 | }).then(done).catch(done) 54 | } 55 | } 56 | }, [42], { 57 | chanFilter: { 58 | symbol: 'tETHUSD' 59 | } 60 | }) 61 | 62 | done() 63 | }) 64 | 65 | it('updates state with provided trade if matched', (done) => { 66 | onDataTrades({ 67 | state: { 68 | args 69 | }, 70 | 71 | h: { 72 | debug: () => {}, 73 | updateState: (instance, state) => { 74 | return new Promise((resolve) => { 75 | assert.equal(state.lastTrade, 42) 76 | resolve() 77 | }).then(done).catch(done) 78 | } 79 | } 80 | }, [42], { 81 | chanFilter: { 82 | symbol: 'tBTCUSD' 83 | } 84 | }) 85 | }) 86 | }) 87 | -------------------------------------------------------------------------------- /test/twap/events/life_start.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict' 3 | 4 | const assert = require('assert') 5 | const Promise = require('bluebird') 6 | const onLifeStart = require('twap/events/life_start') 7 | const Config = require('twap/config') 8 | 9 | describe('twap:events:life_start', () => { 10 | it('sets up interval & saves it on state', (done) => { 11 | let interval = null 12 | 13 | onLifeStart({ 14 | state: { 15 | args: { 16 | sliceInterval: 0, 17 | priceTarget: 1000, 18 | priceCondition: Config.PRICE_COND.MATCH_SIDE 19 | } 20 | }, 21 | 22 | h: { 23 | debug: () => {}, 24 | 25 | updateState: (instance, state) => { 26 | return new Promise((resolve) => { 27 | assert(state.interval) 28 | interval = state.interval 29 | resolve() 30 | }).catch(done) 31 | }, 32 | 33 | emitSelf: (eName) => { 34 | return new Promise((resolve) => { 35 | assert.equal(eName, 'interval_tick') 36 | assert(interval) 37 | clearInterval(interval) 38 | resolve() 39 | }).then(done).catch(done) 40 | } 41 | } 42 | }) 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /test/twap/events/life_stop.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict' 3 | 4 | const onLifeStop = require('twap/events/life_stop') 5 | 6 | describe('twap:events:life_stop', () => { 7 | it('sets up interval & saves it on state', (done) => { 8 | const interval = setInterval(() => { 9 | done(new Error('interval should not have been set')) 10 | }, 10) 11 | 12 | onLifeStop({ 13 | state: { interval } 14 | }) 15 | 16 | setTimeout(done, 50) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /test/twap/events/orders_order_cancel.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict' 3 | 4 | const assert = require('assert') 5 | const onOrderCancel = require('twap/events/orders_order_cancel') 6 | 7 | describe('twap:events:orders_order_cancel', () => { 8 | it('submits all known orders for cancellation & stops operation', (done) => { 9 | let call = 0 10 | const orderState = { 11 | 1: 'some_order_object' 12 | } 13 | 14 | onOrderCancel({ 15 | state: { 16 | gid: 100, 17 | args: { cancelDelay: 42 }, 18 | orders: orderState 19 | }, 20 | 21 | h: { 22 | debug: () => {}, 23 | emit: async (eName, gid, orders, cancelDelay) => { 24 | if (call === 0) { 25 | assert.equal(gid, 100) 26 | assert.equal(eName, 'exec:order:cancel:all') 27 | assert.equal(cancelDelay, 42) 28 | assert.deepStrictEqual(orders, orderState) 29 | call += 1 30 | } else if (call === 1) { 31 | assert.equal(eName, 'exec:stop') 32 | done() 33 | } else { 34 | done(new Error('too many events emitted')) 35 | } 36 | } 37 | } 38 | }) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /test/twap/events/orders_order_fill.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict' 3 | 4 | const assert = require('assert') 5 | const Promise = require('bluebird') 6 | const onOrderFill = require('twap/events/orders_order_fill') 7 | 8 | describe('twap:events:orders_order_fill', () => { 9 | const orderState = { 1: 'some_order_object' } 10 | const instance = { 11 | state: { 12 | gid: 100, 13 | orders: orderState, 14 | args: { 15 | amount: 100, 16 | cancelDelay: 42 17 | } 18 | }, 19 | 20 | h: { 21 | debug: () => {}, 22 | updateState: async () => {}, 23 | emitSelf: async () => {}, 24 | emit: async () => {} 25 | } 26 | } 27 | 28 | const filledOrder = { 29 | getLastFillAmount: () => { 30 | return 42 31 | } 32 | } 33 | 34 | it('updates remaining amount w/ fill amount', (done) => { 35 | onOrderFill({ 36 | ...instance, 37 | state: { 38 | ...instance.state, 39 | remainingAmount: 100 40 | }, 41 | 42 | h: { 43 | ...instance.h, 44 | 45 | updateState: (inst, update) => { 46 | return new Promise((resolve) => { 47 | assert.deepStrictEqual(update, { 48 | remainingAmount: 58 49 | }) 50 | resolve() 51 | }).then(done).catch(done) 52 | } 53 | } 54 | }, filledOrder) 55 | }) 56 | 57 | it('does not stop if remaining amount is not dust', (done) => { 58 | onOrderFill({ 59 | ...instance, 60 | state: { 61 | ...instance.state, 62 | remainingAmount: 100 63 | }, 64 | 65 | h: { 66 | ...instance.h, 67 | 68 | emit: (eName) => { 69 | return new Promise((resolve, reject) => { 70 | reject(new Error('should not have stopped')) 71 | }).catch(done) 72 | } 73 | } 74 | }, filledOrder) 75 | 76 | done() 77 | }) 78 | 79 | const testStopAmount = (remainingAmount, done) => { 80 | onOrderFill({ 81 | ...instance, 82 | state: { 83 | ...instance.state, 84 | remainingAmount 85 | }, 86 | 87 | h: { 88 | ...instance.h, 89 | 90 | emitSelf: (eName) => { 91 | return new Promise((resolve) => { 92 | throw new Error('should not have submitted') 93 | }).then(done).catch(done) 94 | }, 95 | 96 | emit: (eName) => { 97 | return new Promise((resolve) => { 98 | if (eName === 'exec:stop') { 99 | done() 100 | } 101 | resolve() 102 | }).catch(done) 103 | } 104 | } 105 | }, filledOrder) 106 | } 107 | 108 | it('emits stop event if dust is left', (done) => { 109 | testStopAmount(42.00000001, done) 110 | }) 111 | 112 | it('emits stop event if no amount is left', (done) => { 113 | testStopAmount(42, done) 114 | }) 115 | }) 116 | -------------------------------------------------------------------------------- /test/twap/events/self_interval_tick.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict' 3 | 4 | const assert = require('assert') 5 | const Promise = require('bluebird') 6 | const _isObject = require('lodash/isObject') 7 | const onIntervalTick = require('twap/events/self_interval_tick') 8 | const Config = require('twap/config') 9 | 10 | const args = { 11 | priceTarget: 1000, 12 | tradeBeyondEnd: true, 13 | cancelDelay: 100, 14 | submitDelay: 200, 15 | sliceAmount: 0.1, 16 | amount: 1, 17 | orderType: 'LIMIT' 18 | } 19 | 20 | describe('twap:events:self_interval_tick', () => { 21 | it('cancels if not trading beyond end period and there are orders', (done) => { 22 | onIntervalTick({ 23 | state: { 24 | gid: 100, 25 | orders: { o: 42 }, 26 | args: { 27 | ...args, 28 | tradeBeyondEnd: false 29 | } 30 | }, 31 | 32 | h: { 33 | debug: () => {}, 34 | emit: (eventName, gid, orders, delay) => { 35 | return new Promise((resolve) => { 36 | if (eventName !== 'exec:order:cancel:all') { 37 | return 38 | } 39 | 40 | assert.equal(gid, 100) 41 | assert.deepEqual(orders, { o: 42 }) 42 | assert.equal(delay, 100) 43 | resolve() 44 | }).then(done).catch(done) 45 | } 46 | } 47 | }) 48 | }) 49 | 50 | it('does not submit orders if book price data needed & unavailable', (done) => { 51 | onIntervalTick({ 52 | state: { 53 | gid: 100, 54 | args: { 55 | ...args, 56 | priceCondition: Config.PRICE_COND.MATCH_MIDPOINT 57 | } 58 | }, 59 | 60 | h: { 61 | debug: () => {}, 62 | emit: (eventName, gid, orders, delay) => { 63 | return new Promise((resolve, reject) => { 64 | reject(new Error('should not have submitted orders')) 65 | }).catch(done) 66 | } 67 | } 68 | }) 69 | 70 | done() 71 | }) 72 | 73 | it('does not submit orders if trade price data needed & unavailable', (done) => { 74 | onIntervalTick({ 75 | state: { 76 | gid: 100, 77 | args: { 78 | ...args, 79 | priceCondition: Config.PRICE_COND.MATCH_LAST 80 | } 81 | }, 82 | 83 | h: { 84 | debug: () => {}, 85 | emit: (eventName, gid, orders, delay) => { 86 | return new Promise((resolve, reject) => { 87 | reject(new Error('should not have submitted orders')) 88 | }).catch(done) 89 | } 90 | } 91 | }) 92 | 93 | done() 94 | }) 95 | 96 | it('submits order if book price data needed, available, and matched', (done) => { 97 | onIntervalTick({ 98 | state: { 99 | gid: 100, 100 | lastBook: { 101 | midPrice: () => { return 1000 } 102 | }, 103 | 104 | remainingAmount: 1, 105 | args: { 106 | ...args, 107 | priceCondition: Config.PRICE_COND.MATCH_MIDPOINT 108 | } 109 | }, 110 | 111 | h: { 112 | debug: () => {}, 113 | emit: (eventName, gid, orders, delay) => { 114 | return new Promise((resolve) => { 115 | assert.equal(eventName, 'exec:order:submit:all') 116 | assert.equal(gid, 100) 117 | assert.equal(delay, 200) 118 | assert.equal(orders.length, 1) 119 | 120 | const [order] = orders 121 | assert(_isObject(order)) 122 | assert.equal(order.price, args.priceTarget) 123 | assert.equal(order.amount, args.sliceAmount) 124 | assert.equal(order.type, 'LIMIT') 125 | resolve() 126 | }).then(done).catch(done) 127 | } 128 | } 129 | }) 130 | }) 131 | 132 | it('submits order if trade price data needed, available, and matched', (done) => { 133 | onIntervalTick({ 134 | state: { 135 | gid: 100, 136 | lastTrade: { price: 1000 }, 137 | remainingAmount: 1, 138 | args: { 139 | ...args, 140 | priceCondition: Config.PRICE_COND.MATCH_LAST 141 | } 142 | }, 143 | 144 | h: { 145 | debug: () => {}, 146 | emit: (eventName, gid, orders, delay) => { 147 | return new Promise((resolve) => { 148 | assert.equal(eventName, 'exec:order:submit:all') 149 | assert.equal(gid, 100) 150 | assert.equal(delay, 200) 151 | assert.equal(orders.length, 1) 152 | 153 | const [order] = orders 154 | assert(_isObject(order)) 155 | assert.equal(order.price, args.priceTarget) 156 | assert.equal(order.amount, args.sliceAmount) 157 | assert.equal(order.type, 'LIMIT') 158 | resolve() 159 | }).then(done).catch(done) 160 | } 161 | } 162 | }) 163 | }) 164 | 165 | it('does not submit order if trade price data needed, available, but not matched', (done) => { 166 | onIntervalTick({ 167 | state: { 168 | gid: 100, 169 | lastTrade: { price: 2000 }, 170 | remainingAmount: 1, 171 | args: { 172 | ...args, 173 | priceCondition: Config.PRICE_COND.MATCH_LAST 174 | } 175 | }, 176 | 177 | h: { 178 | debug: () => {}, 179 | emit: (eventName, gid, orders, delay) => { 180 | return new Promise((resolve, reject) => { 181 | reject(new Error('should not have submitted orders')) 182 | }).catch(done) 183 | } 184 | } 185 | }) 186 | 187 | done() 188 | }) 189 | 190 | it('does not submit order if book price data needed, available, but not matched', (done) => { 191 | onIntervalTick({ 192 | state: { 193 | gid: 100, 194 | lastBook: { 195 | midPrice: () => { return 2000 } 196 | }, 197 | 198 | remainingAmount: 1, 199 | args: { 200 | ...args, 201 | priceCondition: Config.PRICE_COND.MATCH_MIDPOINT 202 | } 203 | }, 204 | 205 | h: { 206 | debug: () => {}, 207 | emit: (eventName, gid, orders, delay) => { 208 | return new Promise((resolve, reject) => { 209 | reject(new Error('should not have submitted orders')) 210 | }).catch(done) 211 | } 212 | } 213 | }) 214 | 215 | done() 216 | }) 217 | }) 218 | -------------------------------------------------------------------------------- /test/twap/meta/init_state.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict' 3 | 4 | const assert = require('assert') 5 | const initState = require('twap/meta/init_state') 6 | 7 | describe('twap:meta:init_state', () => { 8 | it('sets initial remainingAmount', () => { 9 | const state = initState({ amount: 42 }) 10 | assert.equal(state.remainingAmount, 42) 11 | }) 12 | 13 | it('saves args on state', () => { 14 | const args = { amount: 42, otherArg: 100 } 15 | const state = initState(args) 16 | assert.deepStrictEqual(state.args, args) 17 | }) 18 | 19 | it('seeds null interval', () => { 20 | const state = initState({ amount: 42 }) 21 | assert.equal(state.interval, null) 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /test/twap/meta/process_params.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict' 3 | 4 | const assert = require('assert') 5 | const _isFinite = require('lodash/isFinite') 6 | const processParams = require('twap/meta/process_params') 7 | 8 | describe('twap:meta:process_params', () => { 9 | it('adds EXCHANGE prefix for non-margin order types', () => { 10 | const exchangeParams = processParams({ orderType: 'LIMIT', _margin: false }) 11 | const marginParams = processParams({ orderType: 'LIMIT', _margin: true }) 12 | 13 | assert.equal(exchangeParams.orderType, 'EXCHANGE LIMIT') 14 | assert.equal(marginParams.orderType, 'LIMIT') 15 | }) 16 | 17 | it('integrates supplied _symbol', () => { 18 | const params = processParams({ symbol: 'tETHUSD', _symbol: 'tBTCUSD' }) 19 | assert.equal(params.symbol, 'tBTCUSD') 20 | }) 21 | 22 | it('provides defaults for cancel & submit delays', () => { 23 | const params = processParams() 24 | assert(_isFinite(params.cancelDelay)) 25 | assert(_isFinite(params.submitDelay)) 26 | }) 27 | 28 | it('negates amount if selling', () => { 29 | const buyParams = processParams({ amount: 1 }) 30 | const sellParams = processParams({ amount: 1, action: 'Sell' }) 31 | 32 | assert.equal(buyParams.amount, 1) 33 | assert.equal(sellParams.amount, -1) 34 | }) 35 | 36 | it('integrates custom price target from price field', () => { 37 | const params = processParams({ 38 | priceTarget: 'custom', 39 | price: 100 40 | }) 41 | 42 | assert.equal(params.priceTarget, 100) 43 | }) 44 | 45 | it('converts slice interval from seconds to ms', () => { 46 | const params = processParams({ sliceInterval: 1 }) 47 | assert.equal(params.sliceInterval, 1000) 48 | }) 49 | 50 | it('takes abs value of price delta if provided', () => { 51 | const params = processParams({ priceDelta: -1 }) 52 | assert.equal(params.priceDelta, 1) 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /test/twap/meta/validate_params.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict' 3 | 4 | const assert = require('assert') 5 | const _isString = require('lodash/isString') 6 | const validateParams = require('twap/meta/validate_params') 7 | 8 | const validParams = { 9 | orderType: 'LIMIT', 10 | amount: 1, 11 | sliceAmount: 0.1, 12 | sliceInterval: 1, 13 | submitDelay: 100, 14 | cancelDelay: 100, 15 | priceTarget: 1000, 16 | priceCondition: 'MATCH_LAST' 17 | } 18 | 19 | describe('twap:meta:validate_params', () => { 20 | it('returns no error on valid params', () => { 21 | assert.equal(validateParams(validParams), null) 22 | }) 23 | 24 | it('returns error on invalid order type', () => { 25 | assert(_isString(validateParams({ 26 | ...validParams, 27 | orderType: 'nope' 28 | }))) 29 | }) 30 | 31 | it('returns error on invalid amount', () => { 32 | assert(_isString(validateParams({ 33 | ...validParams, 34 | amount: 'nope' 35 | }))) 36 | }) 37 | 38 | it('returns error on invalid slice amount', () => { 39 | assert(_isString(validateParams({ 40 | ...validParams, 41 | sliceAmount: 'nope' 42 | }))) 43 | }) 44 | 45 | it('returns error on invalid or negative slice interval', () => { 46 | assert(_isString(validateParams({ 47 | ...validParams, 48 | sliceInterval: 'nope' 49 | }))) 50 | 51 | assert(_isString(validateParams({ 52 | ...validParams, 53 | sliceInterval: -100 54 | }))) 55 | }) 56 | 57 | it('returns error on invalid or negative submit delay', () => { 58 | assert(_isString(validateParams({ 59 | ...validParams, 60 | submitDelay: 'nope' 61 | }))) 62 | 63 | assert(_isString(validateParams({ 64 | ...validParams, 65 | submitDelay: -100 66 | }))) 67 | }) 68 | 69 | it('returns error on invalid or negative cancel delay', () => { 70 | assert(_isString(validateParams({ 71 | ...validParams, 72 | cancelDelay: 'nope' 73 | }))) 74 | 75 | assert(_isString(validateParams({ 76 | ...validParams, 77 | cancelDelay: -100 78 | }))) 79 | }) 80 | 81 | it('returns error if amount & sliceAmount differ in sign', () => { 82 | assert(_isString(validateParams({ 83 | ...validParams, 84 | amount: -1 85 | }))) 86 | }) 87 | 88 | it('returns error on non-numeric and non-string price target', () => { 89 | assert(_isString(validateParams({ 90 | ...validParams, 91 | priceTarget: { nope: 42 } 92 | }))) 93 | }) 94 | 95 | it('returns error on negative explicit price target', () => { 96 | assert(_isString(validateParams({ 97 | ...validParams, 98 | priceTarget: -1 99 | }))) 100 | }) 101 | 102 | it('returns error on numeric price target with invalid price condition', () => { 103 | assert(_isString(validateParams({ 104 | ...validParams, 105 | priceTarget: 100, 106 | priceCondition: 'nope' 107 | }))) 108 | }) 109 | 110 | it('returns error on conditional price target with invalid condition', () => { 111 | assert(_isString(validateParams({ 112 | ...validParams, 113 | priceTarget: 'nope' 114 | }))) 115 | }) 116 | 117 | it('returns error on non-numeric price delta if provided', () => { 118 | assert(_isString(validateParams({ 119 | ...validParams, 120 | priceDelta: 'nope' 121 | }))) 122 | }) 123 | }) 124 | --------------------------------------------------------------------------------