hive-nectar 0.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of hive-nectar might be problematic. Click here for more details.
- hive_nectar-0.0.2.dist-info/METADATA +182 -0
- hive_nectar-0.0.2.dist-info/RECORD +86 -0
- hive_nectar-0.0.2.dist-info/WHEEL +4 -0
- hive_nectar-0.0.2.dist-info/entry_points.txt +2 -0
- hive_nectar-0.0.2.dist-info/licenses/LICENSE.txt +23 -0
- nectar/__init__.py +32 -0
- nectar/account.py +4371 -0
- nectar/amount.py +475 -0
- nectar/asciichart.py +270 -0
- nectar/asset.py +82 -0
- nectar/block.py +446 -0
- nectar/blockchain.py +1178 -0
- nectar/blockchaininstance.py +2284 -0
- nectar/blockchainobject.py +221 -0
- nectar/blurt.py +563 -0
- nectar/cli.py +6285 -0
- nectar/comment.py +1217 -0
- nectar/community.py +513 -0
- nectar/constants.py +111 -0
- nectar/conveyor.py +309 -0
- nectar/discussions.py +1709 -0
- nectar/exceptions.py +149 -0
- nectar/hive.py +546 -0
- nectar/hivesigner.py +420 -0
- nectar/imageuploader.py +72 -0
- nectar/instance.py +129 -0
- nectar/market.py +1013 -0
- nectar/memo.py +449 -0
- nectar/message.py +357 -0
- nectar/nodelist.py +444 -0
- nectar/price.py +557 -0
- nectar/profile.py +65 -0
- nectar/rc.py +308 -0
- nectar/snapshot.py +726 -0
- nectar/steem.py +582 -0
- nectar/storage.py +53 -0
- nectar/transactionbuilder.py +622 -0
- nectar/utils.py +545 -0
- nectar/version.py +2 -0
- nectar/vote.py +557 -0
- nectar/wallet.py +472 -0
- nectar/witness.py +617 -0
- nectarapi/__init__.py +11 -0
- nectarapi/exceptions.py +123 -0
- nectarapi/graphenerpc.py +589 -0
- nectarapi/node.py +178 -0
- nectarapi/noderpc.py +229 -0
- nectarapi/rpcutils.py +97 -0
- nectarapi/version.py +2 -0
- nectarbase/__init__.py +14 -0
- nectarbase/ledgertransactions.py +75 -0
- nectarbase/memo.py +243 -0
- nectarbase/objects.py +429 -0
- nectarbase/objecttypes.py +22 -0
- nectarbase/operationids.py +102 -0
- nectarbase/operations.py +1297 -0
- nectarbase/signedtransactions.py +48 -0
- nectarbase/transactions.py +11 -0
- nectarbase/version.py +2 -0
- nectargrapheneapi/__init__.py +6 -0
- nectargraphenebase/__init__.py +27 -0
- nectargraphenebase/account.py +846 -0
- nectargraphenebase/aes.py +52 -0
- nectargraphenebase/base58.py +192 -0
- nectargraphenebase/bip32.py +494 -0
- nectargraphenebase/bip38.py +134 -0
- nectargraphenebase/chains.py +149 -0
- nectargraphenebase/dictionary.py +3 -0
- nectargraphenebase/ecdsasig.py +326 -0
- nectargraphenebase/objects.py +123 -0
- nectargraphenebase/objecttypes.py +6 -0
- nectargraphenebase/operationids.py +3 -0
- nectargraphenebase/operations.py +23 -0
- nectargraphenebase/prefix.py +11 -0
- nectargraphenebase/py23.py +38 -0
- nectargraphenebase/signedtransactions.py +201 -0
- nectargraphenebase/types.py +419 -0
- nectargraphenebase/unsignedtransactions.py +283 -0
- nectargraphenebase/version.py +2 -0
- nectarstorage/__init__.py +38 -0
- nectarstorage/base.py +306 -0
- nectarstorage/exceptions.py +16 -0
- nectarstorage/interfaces.py +237 -0
- nectarstorage/masterpassword.py +239 -0
- nectarstorage/ram.py +30 -0
- nectarstorage/sqlite.py +334 -0
nectar/market.py
ADDED
|
@@ -0,0 +1,1013 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import logging
|
|
3
|
+
import random
|
|
4
|
+
from datetime import datetime, timedelta, timezone
|
|
5
|
+
|
|
6
|
+
from nectar.instance import shared_blockchain_instance
|
|
7
|
+
from nectarbase import operations
|
|
8
|
+
|
|
9
|
+
from .account import Account
|
|
10
|
+
from .amount import Amount
|
|
11
|
+
from .asset import Asset
|
|
12
|
+
from .price import FilledOrder, Order, Price
|
|
13
|
+
from .utils import (
|
|
14
|
+
addTzInfo,
|
|
15
|
+
assets_from_string,
|
|
16
|
+
formatTimeFromNow,
|
|
17
|
+
formatTimeString,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
REQUEST_MODULE = None
|
|
21
|
+
if not REQUEST_MODULE:
|
|
22
|
+
try:
|
|
23
|
+
import requests
|
|
24
|
+
|
|
25
|
+
REQUEST_MODULE = "requests"
|
|
26
|
+
except ImportError:
|
|
27
|
+
REQUEST_MODULE = None
|
|
28
|
+
log = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Market(dict):
|
|
32
|
+
"""This class allows to easily access Markets on the blockchain for trading, etc.
|
|
33
|
+
|
|
34
|
+
:param Steem blockchain_instance: Steem instance
|
|
35
|
+
:param Asset base: Base asset
|
|
36
|
+
:param Asset quote: Quote asset
|
|
37
|
+
:returns: Blockchain Market
|
|
38
|
+
:rtype: dictionary with overloaded methods
|
|
39
|
+
|
|
40
|
+
Instances of this class are dictionaries that come with additional
|
|
41
|
+
methods (see below) that allow dealing with a market and its
|
|
42
|
+
corresponding functions.
|
|
43
|
+
|
|
44
|
+
This class tries to identify **two** assets as provided in the
|
|
45
|
+
parameters in one of the following forms:
|
|
46
|
+
|
|
47
|
+
* ``base`` and ``quote`` are valid assets (according to :class:`nectar.asset.Asset`)
|
|
48
|
+
* ``base:quote`` separated with ``:``
|
|
49
|
+
* ``base/quote`` separated with ``/``
|
|
50
|
+
* ``base-quote`` separated with ``-``
|
|
51
|
+
|
|
52
|
+
.. note:: Throughout this library, the ``quote`` symbol will be
|
|
53
|
+
presented first (e.g. ``STEEM:SBD`` with ``STEEM`` being the
|
|
54
|
+
quote), while the ``base`` only refers to a secondary asset
|
|
55
|
+
for a trade. This means, if you call
|
|
56
|
+
:func:`nectar.market.Market.sell` or
|
|
57
|
+
:func:`nectar.market.Market.buy`, you will sell/buy **only
|
|
58
|
+
quote** and obtain/pay **only base**.
|
|
59
|
+
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(self, base=None, quote=None, blockchain_instance=None, **kwargs):
|
|
63
|
+
"""
|
|
64
|
+
Init Market
|
|
65
|
+
|
|
66
|
+
:param nectar.steem.Steem blockchain_instance: Steem instance
|
|
67
|
+
:param nectar.asset.Asset base: Base asset
|
|
68
|
+
:param nectar.asset.Asset quote: Quote asset
|
|
69
|
+
"""
|
|
70
|
+
if blockchain_instance is None:
|
|
71
|
+
if kwargs.get("steem_instance"):
|
|
72
|
+
blockchain_instance = kwargs["steem_instance"]
|
|
73
|
+
elif kwargs.get("hive_instance"):
|
|
74
|
+
blockchain_instance = kwargs["hive_instance"]
|
|
75
|
+
self.blockchain = blockchain_instance or shared_blockchain_instance()
|
|
76
|
+
|
|
77
|
+
if quote is None and isinstance(base, str):
|
|
78
|
+
quote_symbol, base_symbol = assets_from_string(base)
|
|
79
|
+
quote = Asset(quote_symbol, blockchain_instance=self.blockchain)
|
|
80
|
+
base = Asset(base_symbol, blockchain_instance=self.blockchain)
|
|
81
|
+
super(Market, self).__init__(
|
|
82
|
+
{"base": base, "quote": quote}, blockchain_instance=self.blockchain
|
|
83
|
+
)
|
|
84
|
+
elif base and quote:
|
|
85
|
+
quote = Asset(quote, blockchain_instance=self.blockchain)
|
|
86
|
+
base = Asset(base, blockchain_instance=self.blockchain)
|
|
87
|
+
super(Market, self).__init__(
|
|
88
|
+
{"base": base, "quote": quote}, blockchain_instance=self.blockchain
|
|
89
|
+
)
|
|
90
|
+
elif base is None and quote is None:
|
|
91
|
+
quote = Asset(self.blockchain.backed_token_symbol, blockchain_instance=self.blockchain)
|
|
92
|
+
base = Asset(self.blockchain.token_symbol, blockchain_instance=self.blockchain)
|
|
93
|
+
super(Market, self).__init__(
|
|
94
|
+
{"base": base, "quote": quote}, blockchain_instance=self.blockchain
|
|
95
|
+
)
|
|
96
|
+
else:
|
|
97
|
+
raise ValueError("Unknown Market config")
|
|
98
|
+
|
|
99
|
+
def get_string(self, separator=":"):
|
|
100
|
+
"""Return a formated string that identifies the market, e.g. ``STEEM:SBD``
|
|
101
|
+
|
|
102
|
+
:param str separator: The separator of the assets (defaults to ``:``)
|
|
103
|
+
"""
|
|
104
|
+
return "%s%s%s" % (self["quote"]["symbol"], separator, self["base"]["symbol"])
|
|
105
|
+
|
|
106
|
+
def __eq__(self, other):
|
|
107
|
+
if isinstance(other, str):
|
|
108
|
+
quote_symbol, base_symbol = assets_from_string(other)
|
|
109
|
+
return (
|
|
110
|
+
self["quote"]["symbol"] == quote_symbol and self["base"]["symbol"] == base_symbol
|
|
111
|
+
) or (self["quote"]["symbol"] == base_symbol and self["base"]["symbol"] == quote_symbol)
|
|
112
|
+
elif isinstance(other, Market):
|
|
113
|
+
return (
|
|
114
|
+
self["quote"]["symbol"] == other["quote"]["symbol"]
|
|
115
|
+
and self["base"]["symbol"] == other["base"]["symbol"]
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def ticker(self, raw_data=False):
|
|
119
|
+
"""Returns the ticker for all markets.
|
|
120
|
+
|
|
121
|
+
Output Parameters:
|
|
122
|
+
|
|
123
|
+
* ``latest``: Price of the order last filled
|
|
124
|
+
* ``lowest_ask``: Price of the lowest ask
|
|
125
|
+
* ``highest_bid``: Price of the highest bid
|
|
126
|
+
* ``sbd_volume``: Volume of SBD
|
|
127
|
+
* ``steem_volume``: Volume of STEEM
|
|
128
|
+
* ``hbd_volume``: Volume of HBD
|
|
129
|
+
* ``hive_volume``: Volume of HIVE
|
|
130
|
+
* ``percent_change``: 24h change percentage (in %)
|
|
131
|
+
|
|
132
|
+
.. note::
|
|
133
|
+
Market is HIVE:HBD and prices are HBD per HIVE!
|
|
134
|
+
|
|
135
|
+
Sample Output:
|
|
136
|
+
|
|
137
|
+
.. code-block:: js
|
|
138
|
+
|
|
139
|
+
{
|
|
140
|
+
'highest_bid': 0.30100226633322913,
|
|
141
|
+
'latest': 0.0,
|
|
142
|
+
'lowest_ask': 0.3249636958897082,
|
|
143
|
+
'percent_change': 0.0,
|
|
144
|
+
'sbd_volume': 108329611.0,
|
|
145
|
+
'steem_volume': 355094043.0
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
"""
|
|
149
|
+
data = {}
|
|
150
|
+
# Core Exchange rate
|
|
151
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(True)
|
|
152
|
+
ticker = self.blockchain.rpc.get_ticker(api="market_history")
|
|
153
|
+
|
|
154
|
+
if raw_data:
|
|
155
|
+
return ticker
|
|
156
|
+
|
|
157
|
+
data["highest_bid"] = Price(
|
|
158
|
+
ticker["highest_bid"],
|
|
159
|
+
base=self["base"],
|
|
160
|
+
quote=self["quote"],
|
|
161
|
+
blockchain_instance=self.blockchain,
|
|
162
|
+
)
|
|
163
|
+
data["latest"] = Price(
|
|
164
|
+
ticker["latest"],
|
|
165
|
+
quote=self["quote"],
|
|
166
|
+
base=self["base"],
|
|
167
|
+
blockchain_instance=self.blockchain,
|
|
168
|
+
)
|
|
169
|
+
data["lowest_ask"] = Price(
|
|
170
|
+
ticker["lowest_ask"],
|
|
171
|
+
base=self["base"],
|
|
172
|
+
quote=self["quote"],
|
|
173
|
+
blockchain_instance=self.blockchain,
|
|
174
|
+
)
|
|
175
|
+
data["percent_change"] = float(ticker["percent_change"])
|
|
176
|
+
if "sbd_volume" in ticker:
|
|
177
|
+
data["sbd_volume"] = Amount(ticker["sbd_volume"], blockchain_instance=self.blockchain)
|
|
178
|
+
elif "hbd_volume" in ticker:
|
|
179
|
+
data["hbd_volume"] = Amount(ticker["hbd_volume"], blockchain_instance=self.blockchain)
|
|
180
|
+
if "steem_volume" in ticker:
|
|
181
|
+
data["steem_volume"] = Amount(
|
|
182
|
+
ticker["steem_volume"], blockchain_instance=self.blockchain
|
|
183
|
+
)
|
|
184
|
+
elif "hive_volume" in ticker:
|
|
185
|
+
data["hive_volume"] = Amount(ticker["hive_volume"], blockchain_instance=self.blockchain)
|
|
186
|
+
|
|
187
|
+
return data
|
|
188
|
+
|
|
189
|
+
def volume24h(self, raw_data=False):
|
|
190
|
+
"""Returns the 24-hour volume for all markets, plus totals for primary currencies.
|
|
191
|
+
|
|
192
|
+
Sample output:
|
|
193
|
+
|
|
194
|
+
.. code-block:: js
|
|
195
|
+
|
|
196
|
+
{
|
|
197
|
+
"STEEM": 361666.63617,
|
|
198
|
+
"SBD": 1087.0
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
"""
|
|
202
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(True)
|
|
203
|
+
volume = self.blockchain.rpc.get_volume(api="market_history")
|
|
204
|
+
if raw_data:
|
|
205
|
+
return volume
|
|
206
|
+
if "sbd_volume" in volume and "steem_volume" in volume:
|
|
207
|
+
return {
|
|
208
|
+
self["base"]["symbol"]: Amount(
|
|
209
|
+
volume["sbd_volume"], blockchain_instance=self.blockchain
|
|
210
|
+
),
|
|
211
|
+
self["quote"]["symbol"]: Amount(
|
|
212
|
+
volume["steem_volume"], blockchain_instance=self.blockchain
|
|
213
|
+
),
|
|
214
|
+
}
|
|
215
|
+
elif "hbd_volume" in volume and "hive_volume" in volume:
|
|
216
|
+
return {
|
|
217
|
+
self["base"]["symbol"]: Amount(
|
|
218
|
+
volume["hbd_volume"], blockchain_instance=self.blockchain
|
|
219
|
+
),
|
|
220
|
+
self["quote"]["symbol"]: Amount(
|
|
221
|
+
volume["hive_volume"], blockchain_instance=self.blockchain
|
|
222
|
+
),
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
def orderbook(self, limit=25, raw_data=False):
|
|
226
|
+
"""Returns the order book for SBD/STEEM market.
|
|
227
|
+
|
|
228
|
+
:param int limit: Limit the amount of orders (default: 25)
|
|
229
|
+
|
|
230
|
+
Sample output (raw_data=False):
|
|
231
|
+
|
|
232
|
+
.. code-block:: none
|
|
233
|
+
|
|
234
|
+
{
|
|
235
|
+
'asks': [
|
|
236
|
+
380.510 STEEM 460.291 SBD @ 1.209669 SBD/STEEM,
|
|
237
|
+
53.785 STEEM 65.063 SBD @ 1.209687 SBD/STEEM
|
|
238
|
+
],
|
|
239
|
+
'bids': [
|
|
240
|
+
0.292 STEEM 0.353 SBD @ 1.208904 SBD/STEEM,
|
|
241
|
+
8.498 STEEM 10.262 SBD @ 1.207578 SBD/STEEM
|
|
242
|
+
],
|
|
243
|
+
'asks_date': [
|
|
244
|
+
datetime.datetime(2018, 4, 30, 21, 7, 24, tzinfo=<UTC>),
|
|
245
|
+
datetime.datetime(2018, 4, 30, 18, 12, 18, tzinfo=<UTC>)
|
|
246
|
+
],
|
|
247
|
+
'bids_date': [
|
|
248
|
+
datetime.datetime(2018, 4, 30, 21, 1, 21, tzinfo=<UTC>),
|
|
249
|
+
datetime.datetime(2018, 4, 30, 20, 38, 21, tzinfo=<UTC>)
|
|
250
|
+
]
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
Sample output (raw_data=True):
|
|
254
|
+
|
|
255
|
+
.. code-block:: js
|
|
256
|
+
|
|
257
|
+
{
|
|
258
|
+
'asks': [
|
|
259
|
+
{
|
|
260
|
+
'order_price': {'base': '8.000 STEEM', 'quote': '9.618 SBD'},
|
|
261
|
+
'real_price': '1.20225000000000004',
|
|
262
|
+
'steem': 4565,
|
|
263
|
+
'sbd': 5488,
|
|
264
|
+
'created': '2018-04-30T21:12:45'
|
|
265
|
+
}
|
|
266
|
+
],
|
|
267
|
+
'bids': [
|
|
268
|
+
{
|
|
269
|
+
'order_price': {'base': '10.000 SBD', 'quote': '8.333 STEEM'},
|
|
270
|
+
'real_price': '1.20004800192007677',
|
|
271
|
+
'steem': 8333,
|
|
272
|
+
'sbd': 10000,
|
|
273
|
+
'created': '2018-04-30T20:29:33'
|
|
274
|
+
}
|
|
275
|
+
]
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.. note:: Each bid is an instance of
|
|
279
|
+
class:`nectar.price.Order` and thus carries the keys
|
|
280
|
+
``base``, ``quote`` and ``price``. From those you can
|
|
281
|
+
obtain the actual amounts for sale
|
|
282
|
+
|
|
283
|
+
"""
|
|
284
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(True)
|
|
285
|
+
if self.blockchain.rpc.get_use_appbase():
|
|
286
|
+
orders = self.blockchain.rpc.get_order_book({"limit": limit}, api="market_history")
|
|
287
|
+
else:
|
|
288
|
+
orders = self.blockchain.rpc.get_order_book(limit, api="database_api")
|
|
289
|
+
if raw_data:
|
|
290
|
+
return orders
|
|
291
|
+
asks = list(
|
|
292
|
+
[
|
|
293
|
+
Order(
|
|
294
|
+
Amount(x["order_price"]["quote"], blockchain_instance=self.blockchain),
|
|
295
|
+
Amount(x["order_price"]["base"], blockchain_instance=self.blockchain),
|
|
296
|
+
blockchain_instance=self.blockchain,
|
|
297
|
+
)
|
|
298
|
+
for x in orders["asks"]
|
|
299
|
+
]
|
|
300
|
+
)
|
|
301
|
+
bids = list(
|
|
302
|
+
[
|
|
303
|
+
Order(
|
|
304
|
+
Amount(x["order_price"]["quote"], blockchain_instance=self.blockchain),
|
|
305
|
+
Amount(x["order_price"]["base"], blockchain_instance=self.blockchain),
|
|
306
|
+
blockchain_instance=self.blockchain,
|
|
307
|
+
).invert()
|
|
308
|
+
for x in orders["bids"]
|
|
309
|
+
]
|
|
310
|
+
)
|
|
311
|
+
asks_date = list([formatTimeString(x["created"]) for x in orders["asks"]])
|
|
312
|
+
bids_date = list([formatTimeString(x["created"]) for x in orders["bids"]])
|
|
313
|
+
data = {"asks": asks, "bids": bids, "asks_date": asks_date, "bids_date": bids_date}
|
|
314
|
+
return data
|
|
315
|
+
|
|
316
|
+
def recent_trades(self, limit=25, raw_data=False):
|
|
317
|
+
"""Returns the order book for a given market. You may also
|
|
318
|
+
specify "all" to get the orderbooks of all markets.
|
|
319
|
+
|
|
320
|
+
:param int limit: Limit the amount of orders (default: 25)
|
|
321
|
+
:param bool raw_data: when False, FilledOrder objects will be
|
|
322
|
+
returned
|
|
323
|
+
|
|
324
|
+
Sample output (raw_data=False):
|
|
325
|
+
|
|
326
|
+
.. code-block:: none
|
|
327
|
+
|
|
328
|
+
[
|
|
329
|
+
(2018-04-30 21:00:54+00:00) 0.267 STEEM 0.323 SBD @ 1.209738 SBD/STEEM,
|
|
330
|
+
(2018-04-30 20:59:30+00:00) 0.131 STEEM 0.159 SBD @ 1.213740 SBD/STEEM,
|
|
331
|
+
(2018-04-30 20:55:45+00:00) 0.093 STEEM 0.113 SBD @ 1.215054 SBD/STEEM,
|
|
332
|
+
(2018-04-30 20:55:30+00:00) 26.501 STEEM 32.058 SBD @ 1.209690 SBD/STEEM,
|
|
333
|
+
(2018-04-30 20:55:18+00:00) 2.108 STEEM 2.550 SBD @ 1.209677 SBD/STEEM,
|
|
334
|
+
]
|
|
335
|
+
|
|
336
|
+
Sample output (raw_data=True):
|
|
337
|
+
|
|
338
|
+
.. code-block:: js
|
|
339
|
+
|
|
340
|
+
[
|
|
341
|
+
{'date': '2018-04-30T21:02:45', 'current_pays': '0.235 SBD', 'open_pays': '0.194 STEEM'},
|
|
342
|
+
{'date': '2018-04-30T21:02:03', 'current_pays': '24.494 SBD', 'open_pays': '20.248 STEEM'},
|
|
343
|
+
{'date': '2018-04-30T20:48:30', 'current_pays': '175.464 STEEM', 'open_pays': '211.955 SBD'},
|
|
344
|
+
{'date': '2018-04-30T20:48:30', 'current_pays': '0.999 STEEM', 'open_pays': '1.207 SBD'},
|
|
345
|
+
{'date': '2018-04-30T20:47:54', 'current_pays': '0.273 SBD', 'open_pays': '0.225 STEEM'},
|
|
346
|
+
]
|
|
347
|
+
|
|
348
|
+
.. note:: Each bid is an instance of
|
|
349
|
+
:class:`nectar.price.Order` and thus carries the keys
|
|
350
|
+
``base``, ``quote`` and ``price``. From those you can
|
|
351
|
+
obtain the actual amounts for sale
|
|
352
|
+
|
|
353
|
+
"""
|
|
354
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(limit > 0)
|
|
355
|
+
if self.blockchain.rpc.get_use_appbase():
|
|
356
|
+
orders = self.blockchain.rpc.get_recent_trades({"limit": limit}, api="market_history")[
|
|
357
|
+
"trades"
|
|
358
|
+
]
|
|
359
|
+
else:
|
|
360
|
+
orders = self.blockchain.rpc.get_recent_trades(limit, api="market_history")
|
|
361
|
+
if raw_data:
|
|
362
|
+
return orders
|
|
363
|
+
filled_order = list([FilledOrder(x, blockchain_instance=self.blockchain) for x in orders])
|
|
364
|
+
return filled_order
|
|
365
|
+
|
|
366
|
+
def trade_history(self, start=None, stop=None, intervall=None, limit=25, raw_data=False):
|
|
367
|
+
"""Returns the trade history for the internal market
|
|
368
|
+
|
|
369
|
+
This function allows to fetch a fixed number of trades at fixed
|
|
370
|
+
intervall times to reduce the call duration time. E.g. it is possible to
|
|
371
|
+
receive the trades from the last 7 days, by fetching 100 trades each 6 hours.
|
|
372
|
+
|
|
373
|
+
When intervall is set to None, all trades are received between start and stop.
|
|
374
|
+
This can take a while.
|
|
375
|
+
|
|
376
|
+
:param datetime start: Start date
|
|
377
|
+
:param datetime stop: Stop date
|
|
378
|
+
:param timedelta intervall: Defines the intervall
|
|
379
|
+
:param int limit: Defines how many trades are fetched at each intervall point
|
|
380
|
+
:param bool raw_data: when True, the raw data are returned
|
|
381
|
+
"""
|
|
382
|
+
utc = timezone.utc
|
|
383
|
+
if not stop:
|
|
384
|
+
stop = utc.localize(datetime.now())
|
|
385
|
+
if not start:
|
|
386
|
+
start = stop - timedelta(hours=1)
|
|
387
|
+
start = addTzInfo(start)
|
|
388
|
+
stop = addTzInfo(stop)
|
|
389
|
+
current_start = start
|
|
390
|
+
filled_order = []
|
|
391
|
+
fo = self.trades(start=current_start, stop=stop, limit=limit, raw_data=raw_data)
|
|
392
|
+
if intervall is None and len(fo) > 0:
|
|
393
|
+
current_start = fo[-1]["date"]
|
|
394
|
+
filled_order += fo
|
|
395
|
+
elif intervall is not None:
|
|
396
|
+
current_start += intervall
|
|
397
|
+
filled_order += [fo]
|
|
398
|
+
last_date = fo[-1]["date"]
|
|
399
|
+
while len(fo) > 0 and last_date < stop:
|
|
400
|
+
fo = self.trades(start=current_start, stop=stop, limit=limit, raw_data=raw_data)
|
|
401
|
+
if len(fo) == 0 or fo[-1]["date"] == last_date:
|
|
402
|
+
break
|
|
403
|
+
last_date = fo[-1]["date"]
|
|
404
|
+
if intervall is None:
|
|
405
|
+
current_start = last_date
|
|
406
|
+
filled_order += fo
|
|
407
|
+
else:
|
|
408
|
+
current_start += intervall
|
|
409
|
+
filled_order += [fo]
|
|
410
|
+
return filled_order
|
|
411
|
+
|
|
412
|
+
def trades(self, limit=100, start=None, stop=None, raw_data=False):
|
|
413
|
+
"""Returns your trade history for a given market.
|
|
414
|
+
|
|
415
|
+
:param int limit: Limit the amount of orders (default: 100)
|
|
416
|
+
:param datetime start: start time
|
|
417
|
+
:param datetime stop: stop time
|
|
418
|
+
|
|
419
|
+
"""
|
|
420
|
+
# FIXME, this call should also return whether it was a buy or
|
|
421
|
+
# sell
|
|
422
|
+
utc = timezone.utc
|
|
423
|
+
if not stop:
|
|
424
|
+
stop = utc.localize(datetime.now())
|
|
425
|
+
if not start:
|
|
426
|
+
start = stop - timedelta(hours=24)
|
|
427
|
+
start = addTzInfo(start)
|
|
428
|
+
stop = addTzInfo(stop)
|
|
429
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(False)
|
|
430
|
+
if self.blockchain.rpc.get_use_appbase():
|
|
431
|
+
orders = self.blockchain.rpc.get_trade_history(
|
|
432
|
+
{"start": formatTimeString(start), "end": formatTimeString(stop), "limit": limit},
|
|
433
|
+
api="market_history",
|
|
434
|
+
)["trades"]
|
|
435
|
+
else:
|
|
436
|
+
orders = self.blockchain.rpc.get_trade_history(
|
|
437
|
+
formatTimeString(start), formatTimeString(stop), limit, api="market_history"
|
|
438
|
+
)
|
|
439
|
+
if raw_data:
|
|
440
|
+
return orders
|
|
441
|
+
filled_order = list([FilledOrder(x, blockchain_instance=self.blockchain) for x in orders])
|
|
442
|
+
return filled_order
|
|
443
|
+
|
|
444
|
+
def market_history_buckets(self):
|
|
445
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(True)
|
|
446
|
+
ret = self.blockchain.rpc.get_market_history_buckets(api="market_history")
|
|
447
|
+
if self.blockchain.rpc.get_use_appbase():
|
|
448
|
+
return ret["bucket_sizes"]
|
|
449
|
+
else:
|
|
450
|
+
return ret
|
|
451
|
+
|
|
452
|
+
def market_history(self, bucket_seconds=300, start_age=3600, end_age=0, raw_data=False):
|
|
453
|
+
"""Return the market history (filled orders).
|
|
454
|
+
|
|
455
|
+
:param int bucket_seconds: Bucket size in seconds (see
|
|
456
|
+
`returnMarketHistoryBuckets()`)
|
|
457
|
+
:param int start_age: Age (in seconds) of the start of the
|
|
458
|
+
window (default: 1h/3600)
|
|
459
|
+
:param int end_age: Age (in seconds) of the end of the window
|
|
460
|
+
(default: now/0)
|
|
461
|
+
:param bool raw_data: (optional) returns raw data if set True
|
|
462
|
+
|
|
463
|
+
Example:
|
|
464
|
+
|
|
465
|
+
.. code-block:: js
|
|
466
|
+
|
|
467
|
+
{
|
|
468
|
+
'close_sbd': 2493387,
|
|
469
|
+
'close_steem': 7743431,
|
|
470
|
+
'high_sbd': 1943872,
|
|
471
|
+
'high_steem': 5999610,
|
|
472
|
+
'id': '7.1.5252',
|
|
473
|
+
'low_sbd': 534928,
|
|
474
|
+
'low_steem': 1661266,
|
|
475
|
+
'open': '2016-07-08T11:25:00',
|
|
476
|
+
'open_sbd': 534928,
|
|
477
|
+
'open_steem': 1661266,
|
|
478
|
+
'sbd_volume': 9714435,
|
|
479
|
+
'seconds': 300,
|
|
480
|
+
'steem_volume': 30088443
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
"""
|
|
484
|
+
buckets = self.market_history_buckets()
|
|
485
|
+
if bucket_seconds < 5 and bucket_seconds >= 0:
|
|
486
|
+
bucket_seconds = buckets[bucket_seconds]
|
|
487
|
+
else:
|
|
488
|
+
if bucket_seconds not in buckets:
|
|
489
|
+
raise ValueError("You need select the bucket_seconds from " + str(buckets))
|
|
490
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(False)
|
|
491
|
+
if self.blockchain.rpc.get_use_appbase():
|
|
492
|
+
history = self.blockchain.rpc.get_market_history(
|
|
493
|
+
{
|
|
494
|
+
"bucket_seconds": bucket_seconds,
|
|
495
|
+
"start": formatTimeFromNow(-start_age - end_age),
|
|
496
|
+
"end": formatTimeFromNow(-end_age),
|
|
497
|
+
},
|
|
498
|
+
api="market_history",
|
|
499
|
+
)["buckets"]
|
|
500
|
+
else:
|
|
501
|
+
history = self.blockchain.rpc.get_market_history(
|
|
502
|
+
bucket_seconds,
|
|
503
|
+
formatTimeFromNow(-start_age - end_age),
|
|
504
|
+
formatTimeFromNow(-end_age),
|
|
505
|
+
api="market_history",
|
|
506
|
+
)
|
|
507
|
+
if raw_data:
|
|
508
|
+
return history
|
|
509
|
+
new_history = []
|
|
510
|
+
for h in history:
|
|
511
|
+
if "open" in h and isinstance(h.get("open"), str):
|
|
512
|
+
h["open"] = formatTimeString(h.get("open", "1970-01-01T00:00:00"))
|
|
513
|
+
new_history.append(h)
|
|
514
|
+
return new_history
|
|
515
|
+
|
|
516
|
+
def accountopenorders(self, account=None, raw_data=False):
|
|
517
|
+
"""Returns open Orders
|
|
518
|
+
|
|
519
|
+
:param Account account: Account name or instance of Account to show orders for in this market
|
|
520
|
+
:param bool raw_data: (optional) returns raw data if set True,
|
|
521
|
+
or a list of Order() instances if False (defaults to False)
|
|
522
|
+
"""
|
|
523
|
+
if not account:
|
|
524
|
+
if "default_account" in self.blockchain.config:
|
|
525
|
+
account = self.blockchain.config["default_account"]
|
|
526
|
+
if not account:
|
|
527
|
+
raise ValueError("You need to provide an account")
|
|
528
|
+
account = Account(account, full=True, blockchain_instance=self.blockchain)
|
|
529
|
+
|
|
530
|
+
r = []
|
|
531
|
+
# orders = account["limit_orders"]
|
|
532
|
+
if not self.blockchain.is_connected():
|
|
533
|
+
return None
|
|
534
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(False)
|
|
535
|
+
if self.blockchain.rpc.get_use_appbase():
|
|
536
|
+
orders = self.blockchain.rpc.find_limit_orders(
|
|
537
|
+
{"account": account["name"]}, api="database"
|
|
538
|
+
)["orders"]
|
|
539
|
+
else:
|
|
540
|
+
orders = self.blockchain.rpc.get_open_orders(account["name"])
|
|
541
|
+
if raw_data:
|
|
542
|
+
return orders
|
|
543
|
+
for o in orders:
|
|
544
|
+
order = {}
|
|
545
|
+
order["order"] = Order(
|
|
546
|
+
Amount(o["sell_price"]["base"], blockchain_instance=self.blockchain),
|
|
547
|
+
Amount(o["sell_price"]["quote"], blockchain_instance=self.blockchain),
|
|
548
|
+
blockchain_instance=self.blockchain,
|
|
549
|
+
)
|
|
550
|
+
order["orderid"] = o["orderid"]
|
|
551
|
+
order["created"] = formatTimeString(o["created"])
|
|
552
|
+
r.append(order)
|
|
553
|
+
return r
|
|
554
|
+
|
|
555
|
+
def buy(
|
|
556
|
+
self,
|
|
557
|
+
price,
|
|
558
|
+
amount,
|
|
559
|
+
expiration=None,
|
|
560
|
+
killfill=False,
|
|
561
|
+
account=None,
|
|
562
|
+
orderid=None,
|
|
563
|
+
returnOrderId=False,
|
|
564
|
+
):
|
|
565
|
+
"""Places a buy order in a given market
|
|
566
|
+
|
|
567
|
+
:param float price: price denoted in ``base``/``quote``
|
|
568
|
+
:param number amount: Amount of ``quote`` to buy
|
|
569
|
+
:param number expiration: (optional) expiration time of the order in seconds (defaults to 7 days)
|
|
570
|
+
:param bool killfill: flag that indicates if the order shall be killed if it is not filled (defaults to False)
|
|
571
|
+
:param string account: Account name that executes that order
|
|
572
|
+
:param string returnOrderId: If set to "head" or "irreversible" the call will wait for the tx to appear in
|
|
573
|
+
the head/irreversible block and add the key "orderid" to the tx output
|
|
574
|
+
|
|
575
|
+
Prices/Rates are denoted in 'base', i.e. the SBD_STEEM market
|
|
576
|
+
is priced in STEEM per SBD.
|
|
577
|
+
|
|
578
|
+
**Example:** in the SBD_STEEM market, a price of 300 means
|
|
579
|
+
a SBD is worth 300 STEEM
|
|
580
|
+
|
|
581
|
+
.. note::
|
|
582
|
+
|
|
583
|
+
All prices returned are in the **reversed** orientation as the
|
|
584
|
+
market. I.e. in the STEEM/SBD market, prices are SBD per STEEM.
|
|
585
|
+
That way you can multiply prices with `1.05` to get a +5%.
|
|
586
|
+
|
|
587
|
+
.. warning::
|
|
588
|
+
|
|
589
|
+
Since buy orders are placed as
|
|
590
|
+
limit-sell orders for the base asset,
|
|
591
|
+
you may end up obtaining more of the
|
|
592
|
+
buy asset than you placed the order
|
|
593
|
+
for. Example:
|
|
594
|
+
|
|
595
|
+
* You place and order to buy 10 SBD for 100 STEEM/SBD
|
|
596
|
+
* This means that you actually place a sell order for 1000 STEEM in order to obtain **at least** 10 SBD
|
|
597
|
+
* If an order on the market exists that sells SBD for cheaper, you will end up with more than 10 SBD
|
|
598
|
+
"""
|
|
599
|
+
if not expiration:
|
|
600
|
+
expiration = self.blockchain.config["order-expiration"]
|
|
601
|
+
if not account:
|
|
602
|
+
if "default_account" in self.blockchain.config:
|
|
603
|
+
account = self.blockchain.config["default_account"]
|
|
604
|
+
if not account:
|
|
605
|
+
raise ValueError("You need to provide an account")
|
|
606
|
+
account = Account(account, blockchain_instance=self.blockchain)
|
|
607
|
+
|
|
608
|
+
if isinstance(price, Price):
|
|
609
|
+
price = price.as_base(self["base"]["symbol"])
|
|
610
|
+
|
|
611
|
+
if isinstance(amount, Amount):
|
|
612
|
+
amount = Amount(amount, blockchain_instance=self.blockchain)
|
|
613
|
+
if not amount["asset"]["symbol"] == self["quote"]["symbol"]:
|
|
614
|
+
raise AssertionError(
|
|
615
|
+
"Price: {} does not match amount: {}".format(str(price), str(amount))
|
|
616
|
+
)
|
|
617
|
+
elif isinstance(amount, str):
|
|
618
|
+
amount = Amount(amount, blockchain_instance=self.blockchain)
|
|
619
|
+
else:
|
|
620
|
+
amount = Amount(amount, self["quote"]["symbol"], blockchain_instance=self.blockchain)
|
|
621
|
+
order = operations.Limit_order_create(
|
|
622
|
+
**{
|
|
623
|
+
"owner": account["name"],
|
|
624
|
+
"orderid": orderid or random.getrandbits(32),
|
|
625
|
+
"amount_to_sell": Amount(
|
|
626
|
+
float(amount) * float(price),
|
|
627
|
+
self["base"]["symbol"],
|
|
628
|
+
blockchain_instance=self.blockchain,
|
|
629
|
+
),
|
|
630
|
+
"min_to_receive": Amount(
|
|
631
|
+
float(amount), self["quote"]["symbol"], blockchain_instance=self.blockchain
|
|
632
|
+
),
|
|
633
|
+
"expiration": formatTimeFromNow(expiration),
|
|
634
|
+
"fill_or_kill": killfill,
|
|
635
|
+
"prefix": self.blockchain.prefix,
|
|
636
|
+
"json_str": not bool(self.blockchain.config["use_condenser"]),
|
|
637
|
+
}
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
if returnOrderId:
|
|
641
|
+
# Make blocking broadcasts
|
|
642
|
+
prevblocking = self.blockchain.blocking
|
|
643
|
+
self.blockchain.blocking = returnOrderId
|
|
644
|
+
|
|
645
|
+
tx = self.blockchain.finalizeOp(order, account["name"], "active")
|
|
646
|
+
|
|
647
|
+
if returnOrderId:
|
|
648
|
+
tx["orderid"] = tx["operation_results"][0][1]
|
|
649
|
+
self.blockchain.blocking = prevblocking
|
|
650
|
+
|
|
651
|
+
return tx
|
|
652
|
+
|
|
653
|
+
def sell(
|
|
654
|
+
self,
|
|
655
|
+
price,
|
|
656
|
+
amount,
|
|
657
|
+
expiration=None,
|
|
658
|
+
killfill=False,
|
|
659
|
+
account=None,
|
|
660
|
+
orderid=None,
|
|
661
|
+
returnOrderId=False,
|
|
662
|
+
):
|
|
663
|
+
"""Places a sell order in a given market
|
|
664
|
+
|
|
665
|
+
:param float price: price denoted in ``base``/``quote``
|
|
666
|
+
:param number amount: Amount of ``quote`` to sell
|
|
667
|
+
:param number expiration: (optional) expiration time of the order in seconds (defaults to 7 days)
|
|
668
|
+
:param bool killfill: flag that indicates if the order shall be killed if it is not filled (defaults to False)
|
|
669
|
+
:param string account: Account name that executes that order
|
|
670
|
+
:param string returnOrderId: If set to "head" or "irreversible" the call will wait for the tx to appear in
|
|
671
|
+
the head/irreversible block and add the key "orderid" to the tx output
|
|
672
|
+
|
|
673
|
+
Prices/Rates are denoted in 'base', i.e. the SBD_STEEM market
|
|
674
|
+
is priced in STEEM per SBD.
|
|
675
|
+
|
|
676
|
+
**Example:** in the SBD_STEEM market, a price of 300 means
|
|
677
|
+
a SBD is worth 300 STEEM
|
|
678
|
+
|
|
679
|
+
.. note::
|
|
680
|
+
|
|
681
|
+
All prices returned are in the **reversed** orientation as the
|
|
682
|
+
market. I.e. in the STEEM/SBD market, prices are SBD per STEEM.
|
|
683
|
+
That way you can multiply prices with `1.05` to get a +5%.
|
|
684
|
+
"""
|
|
685
|
+
if not expiration:
|
|
686
|
+
expiration = self.blockchain.config["order-expiration"]
|
|
687
|
+
if not account:
|
|
688
|
+
if "default_account" in self.blockchain.config:
|
|
689
|
+
account = self.blockchain.config["default_account"]
|
|
690
|
+
if not account:
|
|
691
|
+
raise ValueError("You need to provide an account")
|
|
692
|
+
account = Account(account, blockchain_instance=self.blockchain)
|
|
693
|
+
if isinstance(price, Price):
|
|
694
|
+
price = price.as_base(self["base"]["symbol"])
|
|
695
|
+
|
|
696
|
+
if isinstance(amount, Amount):
|
|
697
|
+
amount = Amount(amount, blockchain_instance=self.blockchain)
|
|
698
|
+
if not amount["asset"]["symbol"] == self["quote"]["symbol"]:
|
|
699
|
+
raise AssertionError(
|
|
700
|
+
"Price: {} does not match amount: {}".format(str(price), str(amount))
|
|
701
|
+
)
|
|
702
|
+
elif isinstance(amount, str):
|
|
703
|
+
amount = Amount(amount, blockchain_instance=self.blockchain)
|
|
704
|
+
else:
|
|
705
|
+
amount = Amount(amount, self["quote"]["symbol"], blockchain_instance=self.blockchain)
|
|
706
|
+
order = operations.Limit_order_create(
|
|
707
|
+
**{
|
|
708
|
+
"owner": account["name"],
|
|
709
|
+
"orderid": orderid or random.getrandbits(32),
|
|
710
|
+
"amount_to_sell": Amount(
|
|
711
|
+
float(amount), self["quote"]["symbol"], blockchain_instance=self.blockchain
|
|
712
|
+
),
|
|
713
|
+
"min_to_receive": Amount(
|
|
714
|
+
float(amount) * float(price),
|
|
715
|
+
self["base"]["symbol"],
|
|
716
|
+
blockchain_instance=self.blockchain,
|
|
717
|
+
),
|
|
718
|
+
"expiration": formatTimeFromNow(expiration),
|
|
719
|
+
"fill_or_kill": killfill,
|
|
720
|
+
"prefix": self.blockchain.prefix,
|
|
721
|
+
"json_str": not bool(self.blockchain.config["use_condenser"]),
|
|
722
|
+
}
|
|
723
|
+
)
|
|
724
|
+
if returnOrderId:
|
|
725
|
+
# Make blocking broadcasts
|
|
726
|
+
prevblocking = self.blockchain.blocking
|
|
727
|
+
self.blockchain.blocking = returnOrderId
|
|
728
|
+
|
|
729
|
+
tx = self.blockchain.finalizeOp(order, account["name"], "active")
|
|
730
|
+
|
|
731
|
+
if returnOrderId:
|
|
732
|
+
tx["orderid"] = tx["operation_results"][0][1]
|
|
733
|
+
self.blockchain.blocking = prevblocking
|
|
734
|
+
|
|
735
|
+
return tx
|
|
736
|
+
|
|
737
|
+
def cancel(self, orderNumbers, account=None, **kwargs):
|
|
738
|
+
"""Cancels an order you have placed in a given market. Requires
|
|
739
|
+
only the "orderNumbers".
|
|
740
|
+
|
|
741
|
+
:param orderNumbers: A single order number or a list of order numbers
|
|
742
|
+
:type orderNumbers: int, list
|
|
743
|
+
"""
|
|
744
|
+
if not account:
|
|
745
|
+
if "default_account" in self.blockchain.config:
|
|
746
|
+
account = self.blockchain.config["default_account"]
|
|
747
|
+
if not account:
|
|
748
|
+
raise ValueError("You need to provide an account")
|
|
749
|
+
account = Account(account, full=False, blockchain_instance=self.blockchain)
|
|
750
|
+
|
|
751
|
+
if not isinstance(orderNumbers, (list, set, tuple)):
|
|
752
|
+
orderNumbers = {orderNumbers}
|
|
753
|
+
|
|
754
|
+
op = []
|
|
755
|
+
for order in orderNumbers:
|
|
756
|
+
op.append(
|
|
757
|
+
operations.Limit_order_cancel(
|
|
758
|
+
**{"owner": account["name"], "orderid": order, "prefix": self.blockchain.prefix}
|
|
759
|
+
)
|
|
760
|
+
)
|
|
761
|
+
return self.blockchain.finalizeOp(op, account["name"], "active", **kwargs)
|
|
762
|
+
|
|
763
|
+
@staticmethod
|
|
764
|
+
def _weighted_average(values, weights):
|
|
765
|
+
"""Calculates a weighted average"""
|
|
766
|
+
if not (len(values) == len(weights) and len(weights) > 0):
|
|
767
|
+
raise AssertionError("Length of both array must be the same and greater than zero!")
|
|
768
|
+
return sum(x * y for x, y in zip(values, weights)) / sum(weights)
|
|
769
|
+
|
|
770
|
+
@staticmethod
|
|
771
|
+
def btc_usd_ticker(verbose=False):
|
|
772
|
+
"""Returns the BTC/USD price from bitfinex, gdax, kraken, okcoin and bitstamp. The mean price is
|
|
773
|
+
weighted by the exchange volume.
|
|
774
|
+
"""
|
|
775
|
+
prices = {}
|
|
776
|
+
responses = []
|
|
777
|
+
urls = [
|
|
778
|
+
# "https://api.bitfinex.com/v1/pubticker/BTCUSD",
|
|
779
|
+
# "https://api.gdax.com/products/BTC-USD/ticker",
|
|
780
|
+
# "https://api.kraken.com/0/public/Ticker?pair=XBTUSD",
|
|
781
|
+
# "https://www.okcoin.com/api/v1/ticker.do?symbol=btc_usd",
|
|
782
|
+
# "https://www.bitstamp.net/api/v2/ticker/btcusd/",
|
|
783
|
+
"https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd&include_24hr_vol=true",
|
|
784
|
+
]
|
|
785
|
+
cnt = 0
|
|
786
|
+
while len(prices) == 0 and cnt < 5:
|
|
787
|
+
cnt += 1
|
|
788
|
+
try:
|
|
789
|
+
responses = list(requests.get(u, timeout=30) for u in urls)
|
|
790
|
+
except Exception as e:
|
|
791
|
+
log.debug(str(e))
|
|
792
|
+
|
|
793
|
+
for r in [
|
|
794
|
+
x
|
|
795
|
+
for x in responses
|
|
796
|
+
if hasattr(x, "status_code") and x.status_code == 200 and x.json()
|
|
797
|
+
]:
|
|
798
|
+
try:
|
|
799
|
+
if "bitfinex" in r.url:
|
|
800
|
+
data = r.json()
|
|
801
|
+
prices["bitfinex"] = {
|
|
802
|
+
"price": float(data["last_price"]),
|
|
803
|
+
"volume": float(data["volume"]),
|
|
804
|
+
}
|
|
805
|
+
elif "gdax" in r.url:
|
|
806
|
+
data = r.json()
|
|
807
|
+
prices["gdax"] = {
|
|
808
|
+
"price": float(data["price"]),
|
|
809
|
+
"volume": float(data["volume"]),
|
|
810
|
+
}
|
|
811
|
+
elif "kraken" in r.url:
|
|
812
|
+
data = r.json()["result"]["XXBTZUSD"]["p"]
|
|
813
|
+
prices["kraken"] = {"price": float(data[0]), "volume": float(data[1])}
|
|
814
|
+
elif "okcoin" in r.url:
|
|
815
|
+
data = r.json()["ticker"]
|
|
816
|
+
prices["okcoin"] = {
|
|
817
|
+
"price": float(data["last"]),
|
|
818
|
+
"volume": float(data["vol"]),
|
|
819
|
+
}
|
|
820
|
+
elif "bitstamp" in r.url:
|
|
821
|
+
data = r.json()
|
|
822
|
+
prices["bitstamp"] = {
|
|
823
|
+
"price": float(data["last"]),
|
|
824
|
+
"volume": float(data["volume"]),
|
|
825
|
+
}
|
|
826
|
+
elif "coingecko" in r.url:
|
|
827
|
+
data = r.json()["bitcoin"]
|
|
828
|
+
if "usd_24h_vol" in data:
|
|
829
|
+
volume = float(data["usd_24h_vol"])
|
|
830
|
+
else:
|
|
831
|
+
volume = 1
|
|
832
|
+
prices["coingecko"] = {"price": float(data["usd"]), "volume": volume}
|
|
833
|
+
except KeyError as e:
|
|
834
|
+
log.info(str(e))
|
|
835
|
+
|
|
836
|
+
if verbose:
|
|
837
|
+
print(prices)
|
|
838
|
+
|
|
839
|
+
if len(prices) == 0:
|
|
840
|
+
raise RuntimeError("Obtaining BTC/USD prices has failed from all sources.")
|
|
841
|
+
|
|
842
|
+
# vwap
|
|
843
|
+
return Market._weighted_average(
|
|
844
|
+
[x["price"] for x in prices.values()], [x["volume"] for x in prices.values()]
|
|
845
|
+
)
|
|
846
|
+
|
|
847
|
+
@staticmethod
|
|
848
|
+
def steem_btc_ticker():
|
|
849
|
+
"""Returns the STEEM/BTC price from bittrex, binance, huobi and upbit. The mean price is
|
|
850
|
+
weighted by the exchange volume.
|
|
851
|
+
"""
|
|
852
|
+
prices = {}
|
|
853
|
+
responses = []
|
|
854
|
+
urls = [
|
|
855
|
+
# "https://poloniex.com/public?command=returnTicker",
|
|
856
|
+
# "https://bittrex.com/api/v1.1/public/getmarketsummary?market=BTC-STEEM",
|
|
857
|
+
# "https://api.binance.com/api/v1/ticker/24hr",
|
|
858
|
+
# "https://api.huobi.pro/market/history/kline?period=1day&size=1&symbol=steembtc",
|
|
859
|
+
# "https://crix-api.upbit.com/v1/crix/trades/ticks?code=CRIX.UPBIT.BTC-STEEM&count=1",
|
|
860
|
+
"https://api.coingecko.com/api/v3/simple/price?ids=steem&vs_currencies=btc&include_24hr_vol=true",
|
|
861
|
+
]
|
|
862
|
+
headers = {
|
|
863
|
+
"Content-type": "application/x-www-form-urlencoded",
|
|
864
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.71 Safari/537.36",
|
|
865
|
+
}
|
|
866
|
+
cnt = 0
|
|
867
|
+
while len(prices) == 0 and cnt < 5:
|
|
868
|
+
cnt += 1
|
|
869
|
+
try:
|
|
870
|
+
responses = list(requests.get(u, headers=headers, timeout=30) for u in urls)
|
|
871
|
+
except Exception as e:
|
|
872
|
+
log.debug(str(e))
|
|
873
|
+
|
|
874
|
+
for r in [
|
|
875
|
+
x
|
|
876
|
+
for x in responses
|
|
877
|
+
if hasattr(x, "status_code") and x.status_code == 200 and x.json()
|
|
878
|
+
]:
|
|
879
|
+
try:
|
|
880
|
+
if "poloniex" in r.url:
|
|
881
|
+
data = r.json()["BTC_STEEM"]
|
|
882
|
+
prices["poloniex"] = {
|
|
883
|
+
"price": float(data["last"]),
|
|
884
|
+
"volume": float(data["baseVolume"]),
|
|
885
|
+
}
|
|
886
|
+
elif "bittrex" in r.url:
|
|
887
|
+
data = r.json()["result"][0]
|
|
888
|
+
price = (data["Bid"] + data["Ask"]) / 2
|
|
889
|
+
prices["bittrex"] = {"price": price, "volume": data["BaseVolume"]}
|
|
890
|
+
elif "binance" in r.url:
|
|
891
|
+
data = [x for x in r.json() if x["symbol"] == "STEEMBTC"][0]
|
|
892
|
+
prices["binance"] = {
|
|
893
|
+
"price": float(data["lastPrice"]),
|
|
894
|
+
"volume": float(data["quoteVolume"]),
|
|
895
|
+
}
|
|
896
|
+
elif "huobi" in r.url:
|
|
897
|
+
data = r.json()["data"][-1]
|
|
898
|
+
prices["huobi"] = {
|
|
899
|
+
"price": float(data["close"]),
|
|
900
|
+
"volume": float(data["vol"]),
|
|
901
|
+
}
|
|
902
|
+
elif "upbit" in r.url:
|
|
903
|
+
data = r.json()[-1]
|
|
904
|
+
prices["upbit"] = {
|
|
905
|
+
"price": float(data["tradePrice"]),
|
|
906
|
+
"volume": float(data["tradeVolume"]),
|
|
907
|
+
}
|
|
908
|
+
elif "coingecko" in r.url:
|
|
909
|
+
data = r.json()["steem"]
|
|
910
|
+
if "usd_24h_vol" in data:
|
|
911
|
+
volume = float(data["usd_24h_vol"])
|
|
912
|
+
else:
|
|
913
|
+
volume = 1
|
|
914
|
+
prices["coingecko"] = {"price": float(data["btc"]), "volume": volume}
|
|
915
|
+
except KeyError as e:
|
|
916
|
+
log.info(str(e))
|
|
917
|
+
|
|
918
|
+
if len(prices) == 0:
|
|
919
|
+
raise RuntimeError("Obtaining STEEM/BTC prices has failed from all sources.")
|
|
920
|
+
|
|
921
|
+
return Market._weighted_average(
|
|
922
|
+
[x["price"] for x in prices.values()], [x["volume"] for x in prices.values()]
|
|
923
|
+
)
|
|
924
|
+
|
|
925
|
+
def steem_usd_implied(self):
|
|
926
|
+
"""Returns the current STEEM/USD market price"""
|
|
927
|
+
return self.steem_btc_ticker() * self.btc_usd_ticker()
|
|
928
|
+
|
|
929
|
+
@staticmethod
|
|
930
|
+
def hive_btc_ticker():
|
|
931
|
+
"""Returns the HIVE/BTC price from bittrex and upbit. The mean price is
|
|
932
|
+
weighted by the exchange volume.
|
|
933
|
+
"""
|
|
934
|
+
prices = {}
|
|
935
|
+
responses = []
|
|
936
|
+
urls = [
|
|
937
|
+
# "https://bittrex.com/api/v1.1/public/getmarketsummary?market=BTC-HIVE",
|
|
938
|
+
# "https://api.binance.com/api/v1/ticker/24hr",
|
|
939
|
+
# "https://api.probit.com/api/exchange/v1/ticker?market_ids=HIVE-USDT",
|
|
940
|
+
"https://api.coingecko.com/api/v3/simple/price?ids=hive&vs_currencies=btc&include_24hr_vol=true",
|
|
941
|
+
]
|
|
942
|
+
headers = {
|
|
943
|
+
"Content-type": "application/x-www-form-urlencoded",
|
|
944
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.71 Safari/537.36",
|
|
945
|
+
}
|
|
946
|
+
cnt = 0
|
|
947
|
+
while len(prices) == 0 and cnt < 5:
|
|
948
|
+
cnt += 1
|
|
949
|
+
try:
|
|
950
|
+
responses = list(requests.get(u, headers=headers, timeout=30) for u in urls)
|
|
951
|
+
except Exception as e:
|
|
952
|
+
log.debug(str(e))
|
|
953
|
+
|
|
954
|
+
for r in [
|
|
955
|
+
x
|
|
956
|
+
for x in responses
|
|
957
|
+
if hasattr(x, "status_code") and x.status_code == 200 and x.json()
|
|
958
|
+
]:
|
|
959
|
+
try:
|
|
960
|
+
if "poloniex" in r.url:
|
|
961
|
+
data = r.json()["BTC_HIVE"]
|
|
962
|
+
prices["poloniex"] = {
|
|
963
|
+
"price": float(data["last"]),
|
|
964
|
+
"volume": float(data["baseVolume"]),
|
|
965
|
+
}
|
|
966
|
+
elif "bittrex" in r.url:
|
|
967
|
+
data = r.json()["result"][0]
|
|
968
|
+
price = (data["Bid"] + data["Ask"]) / 2
|
|
969
|
+
prices["bittrex"] = {"price": price, "volume": data["BaseVolume"]}
|
|
970
|
+
elif "binance" in r.url:
|
|
971
|
+
data = [x for x in r.json() if x["symbol"] == "HIVEBTC"][0]
|
|
972
|
+
prices["binance"] = {
|
|
973
|
+
"price": float(data["lastPrice"]),
|
|
974
|
+
"volume": float(data["quoteVolume"]),
|
|
975
|
+
}
|
|
976
|
+
elif "huobi" in r.url:
|
|
977
|
+
data = r.json()["data"][-1]
|
|
978
|
+
prices["huobi"] = {
|
|
979
|
+
"price": float(data["close"]),
|
|
980
|
+
"volume": float(data["vol"]),
|
|
981
|
+
}
|
|
982
|
+
elif "upbit" in r.url:
|
|
983
|
+
data = r.json()[-1]
|
|
984
|
+
prices["upbit"] = {
|
|
985
|
+
"price": float(data["tradePrice"]),
|
|
986
|
+
"volume": float(data["tradeVolume"]),
|
|
987
|
+
}
|
|
988
|
+
elif "probit" in r.url:
|
|
989
|
+
data = r.json()["data"]
|
|
990
|
+
prices["huobi"] = {
|
|
991
|
+
"price": float(data["last"]),
|
|
992
|
+
"volume": float(data["base_volume"]),
|
|
993
|
+
}
|
|
994
|
+
elif "coingecko" in r.url:
|
|
995
|
+
data = r.json()["hive"]
|
|
996
|
+
if "btc_24h_vol":
|
|
997
|
+
volume = float(data["btc_24h_vol"])
|
|
998
|
+
else:
|
|
999
|
+
volume = 1
|
|
1000
|
+
prices["coingecko"] = {"price": float(data["btc"]), "volume": volume}
|
|
1001
|
+
except KeyError as e:
|
|
1002
|
+
log.info(str(e))
|
|
1003
|
+
|
|
1004
|
+
if len(prices) == 0:
|
|
1005
|
+
raise RuntimeError("Obtaining HIVE/BTC prices has failed from all sources.")
|
|
1006
|
+
|
|
1007
|
+
return Market._weighted_average(
|
|
1008
|
+
[x["price"] for x in prices.values()], [x["volume"] for x in prices.values()]
|
|
1009
|
+
)
|
|
1010
|
+
|
|
1011
|
+
def hive_usd_implied(self):
|
|
1012
|
+
"""Returns the current HIVE/USD market price"""
|
|
1013
|
+
return self.hive_btc_ticker() * self.btc_usd_ticker()
|