├── .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 |
Logs a string to the console, tagged by AO id/gid
6 |Triggeres an event on the 'self' section
9 |Like emitSelf
but operates after a timeout
Triggers a generic event
15 |Like emit
but operates after a timeout
Triggers an UI notification, sent out via the active websocket connection
21 |Object
Cancels the provided order after a delay, and removes it from the active 24 | order set.
25 |Object
Cancels all orders currently on the AO state after the specified delay
28 |Object
Submits an order after a delay, and adds it to the active order set on 31 | the AO state.
32 |Hooks up the listener for a new event on the 'self' section
35 |Object
Assigns a data channel to the provided AO instance
38 |Object
Updates the state for the provided AO instance
41 |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