hive-nectar 0.2.9__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.
- hive_nectar-0.2.9.dist-info/METADATA +194 -0
- hive_nectar-0.2.9.dist-info/RECORD +87 -0
- hive_nectar-0.2.9.dist-info/WHEEL +4 -0
- hive_nectar-0.2.9.dist-info/entry_points.txt +2 -0
- hive_nectar-0.2.9.dist-info/licenses/LICENSE.txt +23 -0
- nectar/__init__.py +37 -0
- nectar/account.py +5076 -0
- nectar/amount.py +553 -0
- nectar/asciichart.py +303 -0
- nectar/asset.py +122 -0
- nectar/block.py +574 -0
- nectar/blockchain.py +1242 -0
- nectar/blockchaininstance.py +2590 -0
- nectar/blockchainobject.py +263 -0
- nectar/cli.py +5937 -0
- nectar/comment.py +1552 -0
- nectar/community.py +854 -0
- nectar/constants.py +95 -0
- nectar/discussions.py +1437 -0
- nectar/exceptions.py +152 -0
- nectar/haf.py +381 -0
- nectar/hive.py +630 -0
- nectar/imageuploader.py +114 -0
- nectar/instance.py +113 -0
- nectar/market.py +876 -0
- nectar/memo.py +542 -0
- nectar/message.py +379 -0
- nectar/nodelist.py +309 -0
- nectar/price.py +603 -0
- nectar/profile.py +74 -0
- nectar/py.typed +0 -0
- nectar/rc.py +333 -0
- nectar/snapshot.py +1024 -0
- nectar/storage.py +62 -0
- nectar/transactionbuilder.py +659 -0
- nectar/utils.py +630 -0
- nectar/version.py +3 -0
- nectar/vote.py +722 -0
- nectar/wallet.py +472 -0
- nectar/witness.py +728 -0
- nectarapi/__init__.py +12 -0
- nectarapi/exceptions.py +126 -0
- nectarapi/graphenerpc.py +596 -0
- nectarapi/node.py +194 -0
- nectarapi/noderpc.py +79 -0
- nectarapi/openapi.py +107 -0
- nectarapi/py.typed +0 -0
- nectarapi/rpcutils.py +98 -0
- nectarapi/version.py +3 -0
- nectarbase/__init__.py +15 -0
- nectarbase/ledgertransactions.py +106 -0
- nectarbase/memo.py +242 -0
- nectarbase/objects.py +521 -0
- nectarbase/objecttypes.py +21 -0
- nectarbase/operationids.py +102 -0
- nectarbase/operations.py +1357 -0
- nectarbase/py.typed +0 -0
- nectarbase/signedtransactions.py +89 -0
- nectarbase/transactions.py +11 -0
- nectarbase/version.py +3 -0
- nectargraphenebase/__init__.py +27 -0
- nectargraphenebase/account.py +1121 -0
- nectargraphenebase/aes.py +49 -0
- nectargraphenebase/base58.py +197 -0
- nectargraphenebase/bip32.py +575 -0
- nectargraphenebase/bip38.py +110 -0
- nectargraphenebase/chains.py +15 -0
- nectargraphenebase/dictionary.py +2 -0
- nectargraphenebase/ecdsasig.py +309 -0
- nectargraphenebase/objects.py +130 -0
- nectargraphenebase/objecttypes.py +8 -0
- nectargraphenebase/operationids.py +5 -0
- nectargraphenebase/operations.py +25 -0
- nectargraphenebase/prefix.py +13 -0
- nectargraphenebase/py.typed +0 -0
- nectargraphenebase/signedtransactions.py +221 -0
- nectargraphenebase/types.py +557 -0
- nectargraphenebase/unsignedtransactions.py +288 -0
- nectargraphenebase/version.py +3 -0
- nectarstorage/__init__.py +57 -0
- nectarstorage/base.py +317 -0
- nectarstorage/exceptions.py +15 -0
- nectarstorage/interfaces.py +244 -0
- nectarstorage/masterpassword.py +237 -0
- nectarstorage/py.typed +0 -0
- nectarstorage/ram.py +27 -0
- nectarstorage/sqlite.py +343 -0
nectar/market.py
ADDED
|
@@ -0,0 +1,876 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import random
|
|
3
|
+
from datetime import date, datetime, time, timedelta, timezone
|
|
4
|
+
from typing import Any, Dict, List, Optional, Set, Tuple, Union
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from nectar.instance import shared_blockchain_instance
|
|
9
|
+
from nectarbase import operations
|
|
10
|
+
|
|
11
|
+
from .account import Account
|
|
12
|
+
from .amount import Amount
|
|
13
|
+
from .asset import Asset
|
|
14
|
+
from .price import FilledOrder, Order, Price
|
|
15
|
+
from .utils import addTzInfo, assets_from_string, formatTimeFromNow, formatTimeString
|
|
16
|
+
|
|
17
|
+
log = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Market(dict):
|
|
21
|
+
"""This class allows access to the internal market for trading, etc. (Hive-only).
|
|
22
|
+
|
|
23
|
+
:param Hive blockchain_instance: Hive instance
|
|
24
|
+
:param Asset base: Base asset
|
|
25
|
+
:param Asset quote: Quote asset
|
|
26
|
+
:returns: Blockchain Market
|
|
27
|
+
:rtype: dictionary with overloaded methods
|
|
28
|
+
|
|
29
|
+
Instances of this class are dictionaries that come with additional
|
|
30
|
+
methods (see below) that allow dealing with a market and its
|
|
31
|
+
corresponding functions.
|
|
32
|
+
|
|
33
|
+
This class tries to identify **two** assets as provided in the
|
|
34
|
+
parameters in one of the following forms:
|
|
35
|
+
|
|
36
|
+
* ``base`` and ``quote`` are valid assets (according to :class:`nectar.asset.Asset`)
|
|
37
|
+
* ``base:quote`` separated with ``:``
|
|
38
|
+
* ``base/quote`` separated with ``/``
|
|
39
|
+
* ``base-quote`` separated with ``-``
|
|
40
|
+
|
|
41
|
+
.. note:: Throughout this library, the ``quote`` symbol will be
|
|
42
|
+
presented first (e.g. ``HIVE:HBD`` with ``HIVE`` being the
|
|
43
|
+
quote), while the ``base`` only refers to a secondary asset
|
|
44
|
+
for a trade. This means, if you call
|
|
45
|
+
:func:`nectar.market.Market.sell` or
|
|
46
|
+
:func:`nectar.market.Market.buy`, you will sell/buy **only
|
|
47
|
+
quote** and obtain/pay **only base**.
|
|
48
|
+
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
base: Optional[Union[str, Asset]] = None,
|
|
54
|
+
quote: Optional[Union[str, Asset]] = None,
|
|
55
|
+
blockchain_instance: Optional[Any] = None,
|
|
56
|
+
**kwargs: Any,
|
|
57
|
+
) -> None:
|
|
58
|
+
"""
|
|
59
|
+
Create a Market mapping with "base" and "quote" Asset objects.
|
|
60
|
+
|
|
61
|
+
Supports three initialization modes:
|
|
62
|
+
- Single-string market identifier (e.g., "HBD:HIVE" or "HIVE:HBD"): parsed into quote and base symbols and converted to Asset objects.
|
|
63
|
+
- Explicit base and quote (either Asset instances or values accepted by Asset): each converted to an Asset.
|
|
64
|
+
- No arguments: uses the blockchain instance's default token symbols (token_symbol as base, backed_token_symbol as quote).
|
|
65
|
+
|
|
66
|
+
The resolved Asset objects are stored in the instance as entries "base" and "quote". The blockchain instance used is the provided one or the shared global instance.
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
ValueError: if the combination of arguments does not match any supported initialization mode.
|
|
70
|
+
"""
|
|
71
|
+
self.blockchain = blockchain_instance or shared_blockchain_instance()
|
|
72
|
+
|
|
73
|
+
if quote is None and isinstance(base, str):
|
|
74
|
+
quote_symbol, base_symbol = assets_from_string(base)
|
|
75
|
+
quote = Asset(quote_symbol, blockchain_instance=self.blockchain)
|
|
76
|
+
base = Asset(base_symbol, blockchain_instance=self.blockchain)
|
|
77
|
+
super().__init__({"base": base, "quote": quote}, blockchain_instance=self.blockchain)
|
|
78
|
+
elif base and quote:
|
|
79
|
+
# Handle Asset objects properly without converting to string
|
|
80
|
+
if isinstance(quote, Asset):
|
|
81
|
+
quote_asset = quote
|
|
82
|
+
else:
|
|
83
|
+
quote_asset = Asset(str(quote), blockchain_instance=self.blockchain)
|
|
84
|
+
|
|
85
|
+
if isinstance(base, Asset):
|
|
86
|
+
base_asset = base
|
|
87
|
+
else:
|
|
88
|
+
base_asset = Asset(str(base), blockchain_instance=self.blockchain)
|
|
89
|
+
|
|
90
|
+
super().__init__(
|
|
91
|
+
{"base": base_asset, "quote": quote_asset}, blockchain_instance=self.blockchain
|
|
92
|
+
)
|
|
93
|
+
elif base is None and quote is None:
|
|
94
|
+
quote = Asset(self.blockchain.backed_token_symbol, blockchain_instance=self.blockchain)
|
|
95
|
+
base = Asset(self.blockchain.token_symbol, blockchain_instance=self.blockchain)
|
|
96
|
+
super().__init__({"base": base, "quote": quote}, blockchain_instance=self.blockchain)
|
|
97
|
+
else:
|
|
98
|
+
raise ValueError("Unknown Market config")
|
|
99
|
+
|
|
100
|
+
def get_string(self, separator: str = ":") -> str:
|
|
101
|
+
"""
|
|
102
|
+
Return the market identifier as "QUOTE{separator}BASE" (e.g. "HIVE:HBD").
|
|
103
|
+
|
|
104
|
+
Parameters:
|
|
105
|
+
separator (str): Token placed between quote and base symbols. Defaults to ":".
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
str: Formatted market string in the form "<quote><separator><base>".
|
|
109
|
+
"""
|
|
110
|
+
return "{}{}{}".format(self["quote"]["symbol"], separator, self["base"]["symbol"])
|
|
111
|
+
|
|
112
|
+
def __eq__(self, other: object) -> bool:
|
|
113
|
+
if isinstance(other, str):
|
|
114
|
+
quote_symbol, base_symbol = assets_from_string(other)
|
|
115
|
+
return (
|
|
116
|
+
self["quote"]["symbol"] == quote_symbol and self["base"]["symbol"] == base_symbol
|
|
117
|
+
) or (self["quote"]["symbol"] == base_symbol and self["base"]["symbol"] == quote_symbol)
|
|
118
|
+
if isinstance(other, Market):
|
|
119
|
+
return (
|
|
120
|
+
self["quote"]["symbol"] == other["quote"]["symbol"]
|
|
121
|
+
and self["base"]["symbol"] == other["base"]["symbol"]
|
|
122
|
+
)
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
def ticker(self, raw_data: bool = False) -> Union[Dict[str, Any], Any]:
|
|
126
|
+
"""
|
|
127
|
+
Return the market ticker for this Market (HIVE:HBD).
|
|
128
|
+
|
|
129
|
+
By default returns a dict with Price objects for 'highest_bid', 'latest', and 'lowest_ask',
|
|
130
|
+
a float 'percent_change' (24h), and Amount objects for 'hbd_volume' and 'hive_volume' when present.
|
|
131
|
+
If raw_data is True, returns the unprocessed RPC result.
|
|
132
|
+
|
|
133
|
+
Parameters:
|
|
134
|
+
raw_data (bool): If True, return the raw market_history RPC response instead of mapped objects.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
dict or Any: Mapped ticker dictionary (prices as Price, volumes as Amount) or raw RPC data.
|
|
138
|
+
|
|
139
|
+
Notes:
|
|
140
|
+
Prices are expressed as HBD per HIVE.
|
|
141
|
+
"""
|
|
142
|
+
data = {}
|
|
143
|
+
# Core Exchange rate
|
|
144
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(True)
|
|
145
|
+
ticker = self.blockchain.rpc.get_ticker()
|
|
146
|
+
|
|
147
|
+
if raw_data:
|
|
148
|
+
return ticker
|
|
149
|
+
|
|
150
|
+
data["highest_bid"] = Price(
|
|
151
|
+
ticker["highest_bid"],
|
|
152
|
+
base=self["base"],
|
|
153
|
+
quote=self["quote"],
|
|
154
|
+
blockchain_instance=self.blockchain,
|
|
155
|
+
)
|
|
156
|
+
data["latest"] = Price(
|
|
157
|
+
ticker["latest"],
|
|
158
|
+
quote=self["quote"],
|
|
159
|
+
base=self["base"],
|
|
160
|
+
blockchain_instance=self.blockchain,
|
|
161
|
+
)
|
|
162
|
+
data["lowest_ask"] = Price(
|
|
163
|
+
ticker["lowest_ask"],
|
|
164
|
+
base=self["base"],
|
|
165
|
+
quote=self["quote"],
|
|
166
|
+
blockchain_instance=self.blockchain,
|
|
167
|
+
)
|
|
168
|
+
data["percent_change"] = float(ticker["percent_change"])
|
|
169
|
+
if "hbd_volume" in ticker:
|
|
170
|
+
data["hbd_volume"] = Amount(ticker["hbd_volume"], blockchain_instance=self.blockchain)
|
|
171
|
+
if "hive_volume" in ticker:
|
|
172
|
+
data["hive_volume"] = Amount(ticker["hive_volume"], blockchain_instance=self.blockchain)
|
|
173
|
+
|
|
174
|
+
return data
|
|
175
|
+
|
|
176
|
+
def volume24h(self, raw_data: bool = False) -> Optional[Union[Dict[str, Amount], Any]]:
|
|
177
|
+
"""
|
|
178
|
+
Return 24-hour trading volume for this market.
|
|
179
|
+
|
|
180
|
+
If raw_data is True, returns the raw result from the blockchain `market_history` RPC.
|
|
181
|
+
Otherwise, if the RPC result contains 'hbd_volume' and 'hive_volume', returns a dict mapping
|
|
182
|
+
asset symbols to Amount objects, e.g. { "HBD": Amount(...), "HIVE": Amount(...) }.
|
|
183
|
+
If the expected volume keys are not present, returns None.
|
|
184
|
+
|
|
185
|
+
Parameters:
|
|
186
|
+
raw_data (bool): If True, return the unprocessed RPC response.
|
|
187
|
+
"""
|
|
188
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(True)
|
|
189
|
+
volume = self.blockchain.rpc.get_volume()
|
|
190
|
+
if raw_data:
|
|
191
|
+
return volume
|
|
192
|
+
if "hbd_volume" in volume and "hive_volume" in volume:
|
|
193
|
+
return {
|
|
194
|
+
self.blockchain.backed_token_symbol: Amount(
|
|
195
|
+
volume["hbd_volume"], blockchain_instance=self.blockchain
|
|
196
|
+
),
|
|
197
|
+
self.blockchain.token_symbol: Amount(
|
|
198
|
+
volume["hive_volume"], blockchain_instance=self.blockchain
|
|
199
|
+
),
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
def orderbook(self, limit: int = 25, raw_data: bool = False) -> Union[Dict[str, Any], Any]:
|
|
203
|
+
"""Returns the order book for the HBD/HIVE market.
|
|
204
|
+
|
|
205
|
+
:param int limit: Limit the amount of orders (default: 25)
|
|
206
|
+
|
|
207
|
+
Sample output (raw_data=False):
|
|
208
|
+
|
|
209
|
+
.. code-block:: none
|
|
210
|
+
|
|
211
|
+
{
|
|
212
|
+
'asks': [
|
|
213
|
+
380.510 HIVE 460.291 HBD @ 1.209669 HBD/HIVE,
|
|
214
|
+
53.785 HIVE 65.063 HBD @ 1.209687 HBD/HIVE
|
|
215
|
+
],
|
|
216
|
+
'bids': [
|
|
217
|
+
0.292 HIVE 0.353 HBD @ 1.208904 HBD/HIVE,
|
|
218
|
+
8.498 HIVE 10.262 HBD @ 1.207578 HBD/HIVE
|
|
219
|
+
],
|
|
220
|
+
'asks_date': [
|
|
221
|
+
datetime.datetime(2018, 4, 30, 21, 7, 24, tzinfo=<UTC>),
|
|
222
|
+
datetime.datetime(2018, 4, 30, 18, 12, 18, tzinfo=<UTC>)
|
|
223
|
+
],
|
|
224
|
+
'bids_date': [
|
|
225
|
+
datetime.datetime(2018, 4, 30, 21, 1, 21, tzinfo=<UTC>),
|
|
226
|
+
datetime.datetime(2018, 4, 30, 20, 38, 21, tzinfo=<UTC>)
|
|
227
|
+
]
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
Sample output (raw_data=True):
|
|
231
|
+
|
|
232
|
+
.. code-block:: js
|
|
233
|
+
|
|
234
|
+
{
|
|
235
|
+
'asks': [
|
|
236
|
+
{
|
|
237
|
+
'order_price': {'base': '8.000 HIVE', 'quote': '9.618 HBD'},
|
|
238
|
+
'real_price': '1.20225000000000004',
|
|
239
|
+
'hive': 4565,
|
|
240
|
+
'hbd': 5488,
|
|
241
|
+
'created': '2018-04-30T21:12:45'
|
|
242
|
+
}
|
|
243
|
+
],
|
|
244
|
+
'bids': [
|
|
245
|
+
{
|
|
246
|
+
'order_price': {'base': '10.000 HBD', 'quote': '8.333 HIVE'},
|
|
247
|
+
'real_price': '1.20004800192007677',
|
|
248
|
+
'hive': 8333,
|
|
249
|
+
'hbd': 10000,
|
|
250
|
+
'created': '2018-04-30T20:29:33'
|
|
251
|
+
}
|
|
252
|
+
]
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.. note:: Each bid is an instance of
|
|
256
|
+
class:`nectar.price.Order` and thus carries the keys
|
|
257
|
+
``base``, ``quote`` and ``price``. From those you can
|
|
258
|
+
obtain the actual amounts for sale
|
|
259
|
+
|
|
260
|
+
"""
|
|
261
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(True)
|
|
262
|
+
orders = self.blockchain.rpc.get_order_book({"limit": limit})
|
|
263
|
+
if raw_data:
|
|
264
|
+
return orders
|
|
265
|
+
asks = list(
|
|
266
|
+
[
|
|
267
|
+
Order(
|
|
268
|
+
Amount(x["order_price"]["quote"], blockchain_instance=self.blockchain),
|
|
269
|
+
Amount(x["order_price"]["base"], blockchain_instance=self.blockchain),
|
|
270
|
+
blockchain_instance=self.blockchain,
|
|
271
|
+
)
|
|
272
|
+
for x in orders["asks"]
|
|
273
|
+
]
|
|
274
|
+
)
|
|
275
|
+
bids = list(
|
|
276
|
+
[
|
|
277
|
+
Order(
|
|
278
|
+
Amount(x["order_price"]["quote"], blockchain_instance=self.blockchain),
|
|
279
|
+
Amount(x["order_price"]["base"], blockchain_instance=self.blockchain),
|
|
280
|
+
blockchain_instance=self.blockchain,
|
|
281
|
+
).invert()
|
|
282
|
+
for x in orders["bids"]
|
|
283
|
+
]
|
|
284
|
+
)
|
|
285
|
+
asks_date = list([formatTimeString(x["created"]) for x in orders["asks"]])
|
|
286
|
+
bids_date = list([formatTimeString(x["created"]) for x in orders["bids"]])
|
|
287
|
+
data = {"asks": asks, "bids": bids, "asks_date": asks_date, "bids_date": bids_date}
|
|
288
|
+
return data
|
|
289
|
+
|
|
290
|
+
def recent_trades(
|
|
291
|
+
self, limit: int = 25, raw_data: bool = False
|
|
292
|
+
) -> Union[List[FilledOrder], List[Dict[str, Any]]]:
|
|
293
|
+
"""
|
|
294
|
+
Return recent trades for this market.
|
|
295
|
+
|
|
296
|
+
By default returns up to `limit` most recent trades wrapped as FilledOrder objects; if `raw_data` is True the raw trade dictionaries from the market_history API are returned instead.
|
|
297
|
+
|
|
298
|
+
Parameters:
|
|
299
|
+
limit (int): Maximum number of trades to retrieve (default: 25).
|
|
300
|
+
raw_data (bool): If True, return raw API trade entries; if False, return a list of FilledOrder instances constructed with this market's blockchain instance.
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
list: A list of FilledOrder objects when `raw_data` is False, or a list of raw trade dicts as returned by the market_history API when `raw_data` is True.
|
|
304
|
+
"""
|
|
305
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(limit > 0)
|
|
306
|
+
orders = self.blockchain.rpc.get_recent_trades({"limit": limit})["trades"]
|
|
307
|
+
if raw_data:
|
|
308
|
+
return orders
|
|
309
|
+
filled_order = list([FilledOrder(x, blockchain_instance=self.blockchain) for x in orders])
|
|
310
|
+
return filled_order
|
|
311
|
+
|
|
312
|
+
def trade_history(
|
|
313
|
+
self,
|
|
314
|
+
start: Optional[Union[datetime, date, time]] = None,
|
|
315
|
+
stop: Optional[Union[datetime, date, time]] = None,
|
|
316
|
+
limit: int = 25,
|
|
317
|
+
raw_data: bool = False,
|
|
318
|
+
) -> Union[List[FilledOrder], List[Dict[str, Any]]]:
|
|
319
|
+
"""Returns the trade history for the internal market
|
|
320
|
+
|
|
321
|
+
:param datetime start: Start date
|
|
322
|
+
:param datetime stop: Stop date
|
|
323
|
+
:param int limit: Defines how many trades are fetched at each intervall point
|
|
324
|
+
:param bool raw_data: when True, the raw data are returned
|
|
325
|
+
"""
|
|
326
|
+
if not stop:
|
|
327
|
+
stop = datetime.now(timezone.utc)
|
|
328
|
+
if not start:
|
|
329
|
+
# Ensure stop is a datetime for arithmetic operations
|
|
330
|
+
if isinstance(stop, datetime):
|
|
331
|
+
start = stop - timedelta(hours=1)
|
|
332
|
+
else:
|
|
333
|
+
# Convert date/time to datetime for arithmetic
|
|
334
|
+
if isinstance(stop, date):
|
|
335
|
+
start = datetime.combine(stop, time.min, timezone.utc) - timedelta(hours=1)
|
|
336
|
+
else: # time object
|
|
337
|
+
start = datetime.combine(date.today(), stop, timezone.utc) - timedelta(hours=1)
|
|
338
|
+
# Fetch a single page of trades; callers can page manually if needed.
|
|
339
|
+
return self.trades(start=start, stop=stop, limit=limit, raw_data=raw_data)
|
|
340
|
+
|
|
341
|
+
def trades(
|
|
342
|
+
self,
|
|
343
|
+
limit: int = 100,
|
|
344
|
+
start: Optional[Union[datetime, date, time]] = None,
|
|
345
|
+
stop: Optional[Union[datetime, date, time]] = None,
|
|
346
|
+
raw_data: bool = False,
|
|
347
|
+
) -> Union[List[FilledOrder], List[Dict[str, Any]]]:
|
|
348
|
+
"""Returns your trade history for a given market.
|
|
349
|
+
|
|
350
|
+
:param int limit: Limit the amount of orders (default: 100)
|
|
351
|
+
:param datetime start: start time
|
|
352
|
+
:param datetime stop: stop time
|
|
353
|
+
|
|
354
|
+
"""
|
|
355
|
+
# FIXME, this call should also return whether it was a buy or
|
|
356
|
+
# sell
|
|
357
|
+
if not stop:
|
|
358
|
+
stop = datetime.now(timezone.utc)
|
|
359
|
+
if not start:
|
|
360
|
+
# Ensure stop is a datetime for arithmetic operations
|
|
361
|
+
if isinstance(stop, datetime):
|
|
362
|
+
start = stop - timedelta(hours=24)
|
|
363
|
+
else:
|
|
364
|
+
# Convert date/time to datetime for arithmetic
|
|
365
|
+
if isinstance(stop, date):
|
|
366
|
+
start = datetime.combine(stop, time.min, timezone.utc) - timedelta(hours=24)
|
|
367
|
+
else: # time object
|
|
368
|
+
start = datetime.combine(date.today(), stop, timezone.utc) - timedelta(hours=24)
|
|
369
|
+
start = addTzInfo(start)
|
|
370
|
+
stop = addTzInfo(stop)
|
|
371
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(False)
|
|
372
|
+
orders = self.blockchain.rpc.get_trade_history(
|
|
373
|
+
{
|
|
374
|
+
"start": formatTimeString(start) if start else None,
|
|
375
|
+
"end": formatTimeString(stop) if stop else None,
|
|
376
|
+
"limit": limit,
|
|
377
|
+
},
|
|
378
|
+
)["trades"]
|
|
379
|
+
if raw_data:
|
|
380
|
+
return orders
|
|
381
|
+
filled_order = list([FilledOrder(x, blockchain_instance=self.blockchain) for x in orders])
|
|
382
|
+
return filled_order
|
|
383
|
+
|
|
384
|
+
def market_history_buckets(self) -> List[int]:
|
|
385
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(True)
|
|
386
|
+
ret = self.blockchain.rpc.get_market_history_buckets()
|
|
387
|
+
return ret["bucket_sizes"]
|
|
388
|
+
|
|
389
|
+
def market_history(
|
|
390
|
+
self,
|
|
391
|
+
bucket_seconds: Union[int, float] = 300,
|
|
392
|
+
start_age: int = 3600,
|
|
393
|
+
end_age: int = 0,
|
|
394
|
+
raw_data: bool = False,
|
|
395
|
+
) -> Union[List[Dict[str, Any]], Any]:
|
|
396
|
+
"""
|
|
397
|
+
Return market history buckets for a time window.
|
|
398
|
+
|
|
399
|
+
This fetches aggregated market history buckets (filled orders) for the market over a window defined by start_age and end_age and grouped by bucket_seconds. bucket_seconds may be provided either as a numeric bucket size (seconds) or as an index into available buckets returned by market_history_buckets(). When raw_data is False any bucket "open" timestamp strings are normalized to a consistent formatted datetime string.
|
|
400
|
+
|
|
401
|
+
Parameters:
|
|
402
|
+
bucket_seconds (int): Bucket size in seconds or an index into market_history_buckets().
|
|
403
|
+
start_age (int): Age in seconds from now to the start of the window (default 3600 seconds).
|
|
404
|
+
end_age (int): Age in seconds from now to the end of the window (default 0 = now).
|
|
405
|
+
raw_data (bool): If True, return the raw RPC response without normalizing timestamps.
|
|
406
|
+
|
|
407
|
+
Returns:
|
|
408
|
+
list: A list of bucket dicts (or the raw RPC list when raw_data is True). Each bucket contains fields such as 'open', 'seconds', 'open_hbd', 'close_hbd', 'high_hbd', 'low_hbd', 'hbd_volume', 'open_hive', 'close_hive', 'high_hive', 'low_hive', 'hive_volume', and 'id' when available.
|
|
409
|
+
|
|
410
|
+
Raises:
|
|
411
|
+
ValueError: If bucket_seconds is not a valid bucket size or valid index into available buckets.
|
|
412
|
+
"""
|
|
413
|
+
buckets = self.market_history_buckets()
|
|
414
|
+
if (
|
|
415
|
+
isinstance(bucket_seconds, int)
|
|
416
|
+
and bucket_seconds < len(buckets)
|
|
417
|
+
and bucket_seconds >= 0
|
|
418
|
+
):
|
|
419
|
+
bucket_seconds = buckets[bucket_seconds]
|
|
420
|
+
else:
|
|
421
|
+
if bucket_seconds not in buckets:
|
|
422
|
+
raise ValueError("You need select the bucket_seconds from " + str(buckets))
|
|
423
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(False)
|
|
424
|
+
history = self.blockchain.rpc.get_market_history(
|
|
425
|
+
{
|
|
426
|
+
"bucket_seconds": bucket_seconds,
|
|
427
|
+
"start": formatTimeFromNow(-start_age - end_age),
|
|
428
|
+
"end": formatTimeFromNow(-end_age),
|
|
429
|
+
},
|
|
430
|
+
)["buckets"]
|
|
431
|
+
if raw_data:
|
|
432
|
+
return history
|
|
433
|
+
new_history = []
|
|
434
|
+
for h in history:
|
|
435
|
+
if "open" in h and isinstance(h.get("open"), str):
|
|
436
|
+
h["open"] = formatTimeString(h.get("open", "1970-01-01T00:00:00"))
|
|
437
|
+
new_history.append(h)
|
|
438
|
+
return new_history
|
|
439
|
+
|
|
440
|
+
def accountopenorders(
|
|
441
|
+
self, account: Optional[Union[str, Account]] = None, raw_data: bool = False
|
|
442
|
+
) -> Union[List[Order], List[Dict[str, Any]], None]:
|
|
443
|
+
"""Returns open Orders
|
|
444
|
+
|
|
445
|
+
:param Account account: Account name or instance of Account to show orders for in this market
|
|
446
|
+
:param bool raw_data: (optional) returns raw data if set True,
|
|
447
|
+
or a list of Order() instances if False (defaults to False)
|
|
448
|
+
"""
|
|
449
|
+
if not account:
|
|
450
|
+
if "default_account" in self.blockchain.config:
|
|
451
|
+
account = self.blockchain.config["default_account"]
|
|
452
|
+
if not account:
|
|
453
|
+
raise ValueError("You need to provide an account")
|
|
454
|
+
account = Account(account, full=True, blockchain_instance=self.blockchain)
|
|
455
|
+
|
|
456
|
+
r = []
|
|
457
|
+
# orders = account["limit_orders"]
|
|
458
|
+
if not self.blockchain.is_connected():
|
|
459
|
+
return None
|
|
460
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(False)
|
|
461
|
+
orders = self.blockchain.rpc.find_limit_orders({"account": account["name"]})["orders"]
|
|
462
|
+
if raw_data:
|
|
463
|
+
return orders
|
|
464
|
+
for o in orders:
|
|
465
|
+
order = {}
|
|
466
|
+
order["order"] = Order(
|
|
467
|
+
Amount(o["sell_price"]["base"], blockchain_instance=self.blockchain),
|
|
468
|
+
Amount(o["sell_price"]["quote"], blockchain_instance=self.blockchain),
|
|
469
|
+
blockchain_instance=self.blockchain,
|
|
470
|
+
)
|
|
471
|
+
order["orderid"] = o["orderid"]
|
|
472
|
+
order["created"] = formatTimeString(o["created"])
|
|
473
|
+
r.append(order)
|
|
474
|
+
return r
|
|
475
|
+
|
|
476
|
+
def buy(
|
|
477
|
+
self,
|
|
478
|
+
price: Union[str, Price, float],
|
|
479
|
+
amount: Union[str, Amount],
|
|
480
|
+
expiration: Optional[int] = None,
|
|
481
|
+
killfill: bool = False,
|
|
482
|
+
account: Optional[Union[str, Account]] = None,
|
|
483
|
+
orderid: Optional[int] = None,
|
|
484
|
+
returnOrderId: bool = False,
|
|
485
|
+
) -> Union[Dict[str, Any], str]:
|
|
486
|
+
"""
|
|
487
|
+
Place a buy order (limit order) on this market.
|
|
488
|
+
|
|
489
|
+
Prices are expressed in the market's base/quote orientation (HIVE per HBD in HBD_HIVE). This method submits a limit-order-create operation that effectively places a sell order of the base asset to acquire the requested amount of the quote asset.
|
|
490
|
+
|
|
491
|
+
Parameters:
|
|
492
|
+
price (float or Price): Price expressed in base per quote (e.g., HIVE per HBD).
|
|
493
|
+
amount (number or str or Amount): Amount of the quote asset to buy.
|
|
494
|
+
expiration (int, optional): Order lifetime in seconds (default: configured order-expiration, typically 7 days).
|
|
495
|
+
killfill (bool, optional): If True, set fill_or_kill on the order (defaults to False).
|
|
496
|
+
account (str, optional): Account name that will own and broadcast the order. If omitted, default_account from config is used; a ValueError is raised if none is available.
|
|
497
|
+
orderid (int, optional): Explicit client-side order id. If omitted one is randomly generated.
|
|
498
|
+
returnOrderId (bool or str, optional): If truthy (or set to "head"/"irreversible"), the call will wait for the transaction and attach the assigned order id to the returned transaction under the "orderid" key.
|
|
499
|
+
|
|
500
|
+
Returns:
|
|
501
|
+
dict: The finalized broadcast transaction object returned by the blockchain client. If returnOrderId was used, the dict includes an "orderid" field.
|
|
502
|
+
|
|
503
|
+
Raises:
|
|
504
|
+
ValueError: If no account can be resolved.
|
|
505
|
+
AssertionError: If an Amount is provided whose asset symbol does not match the market quote.
|
|
506
|
+
|
|
507
|
+
Notes:
|
|
508
|
+
- Because buy orders are implemented as limit-sell orders of the base asset, the trade can result in receiving more of the quote asset than requested if matching orders exist at better prices.
|
|
509
|
+
"""
|
|
510
|
+
if not expiration:
|
|
511
|
+
expiration = self.blockchain.config["order-expiration"]
|
|
512
|
+
if not account:
|
|
513
|
+
if "default_account" in self.blockchain.config:
|
|
514
|
+
account = self.blockchain.config["default_account"]
|
|
515
|
+
if not account:
|
|
516
|
+
raise ValueError("You need to provide an account")
|
|
517
|
+
account = Account(account, blockchain_instance=self.blockchain)
|
|
518
|
+
|
|
519
|
+
if isinstance(price, Price):
|
|
520
|
+
price = price.as_base(self["base"]["symbol"])
|
|
521
|
+
|
|
522
|
+
if isinstance(amount, Amount):
|
|
523
|
+
amount = Amount(amount, blockchain_instance=self.blockchain)
|
|
524
|
+
if not amount["asset"]["symbol"] == self["quote"]["symbol"]:
|
|
525
|
+
raise AssertionError(
|
|
526
|
+
"Price: {} does not match amount: {}".format(str(price), str(amount))
|
|
527
|
+
)
|
|
528
|
+
elif isinstance(amount, str):
|
|
529
|
+
amount = Amount(amount, blockchain_instance=self.blockchain)
|
|
530
|
+
else:
|
|
531
|
+
amount = Amount(amount, self["quote"]["symbol"], blockchain_instance=self.blockchain)
|
|
532
|
+
order = operations.Limit_order_create(
|
|
533
|
+
**{
|
|
534
|
+
"owner": account["name"],
|
|
535
|
+
"orderid": orderid or random.getrandbits(32),
|
|
536
|
+
"amount_to_sell": Amount(
|
|
537
|
+
float(amount) * float(price),
|
|
538
|
+
self["base"]["symbol"],
|
|
539
|
+
blockchain_instance=self.blockchain,
|
|
540
|
+
json_str=True,
|
|
541
|
+
),
|
|
542
|
+
"min_to_receive": Amount(
|
|
543
|
+
float(amount),
|
|
544
|
+
self["quote"]["symbol"],
|
|
545
|
+
blockchain_instance=self.blockchain,
|
|
546
|
+
json_str=True,
|
|
547
|
+
),
|
|
548
|
+
"expiration": formatTimeFromNow(expiration),
|
|
549
|
+
"fill_or_kill": killfill,
|
|
550
|
+
"prefix": self.blockchain.prefix,
|
|
551
|
+
"json_str": True,
|
|
552
|
+
}
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
if returnOrderId:
|
|
556
|
+
# Make blocking broadcasts
|
|
557
|
+
prevblocking = self.blockchain.blocking
|
|
558
|
+
self.blockchain.blocking = returnOrderId
|
|
559
|
+
|
|
560
|
+
tx = self.blockchain.finalizeOp(order, account["name"], "active")
|
|
561
|
+
|
|
562
|
+
if returnOrderId:
|
|
563
|
+
tx["orderid"] = tx["operation_results"][0][1]
|
|
564
|
+
self.blockchain.blocking = prevblocking
|
|
565
|
+
|
|
566
|
+
return tx
|
|
567
|
+
|
|
568
|
+
def sell(
|
|
569
|
+
self,
|
|
570
|
+
price: Union[str, Price, float],
|
|
571
|
+
amount: Union[str, Amount],
|
|
572
|
+
expiration: Optional[int] = None,
|
|
573
|
+
killfill: bool = False,
|
|
574
|
+
account: Optional[Union[str, Account]] = None,
|
|
575
|
+
orderid: Optional[int] = None,
|
|
576
|
+
returnOrderId: bool = False,
|
|
577
|
+
) -> Union[Dict[str, Any], str]:
|
|
578
|
+
"""
|
|
579
|
+
Place a limit sell order on this market, selling the market's quote asset for its base asset.
|
|
580
|
+
|
|
581
|
+
This creates a Limit_order_create operation where `amount_to_sell` is the provided `amount` in the market's quote asset and `min_to_receive` is `amount * price` in the market's base asset.
|
|
582
|
+
|
|
583
|
+
Parameters:
|
|
584
|
+
price (float or Price): Price expressed as base per quote (e.g., in HBD_HIVE market, a price of 3 means 1 HBD = 3 HIVE).
|
|
585
|
+
amount (number or str or Amount): Quantity of the quote asset to sell; may be an Amount instance, a string (e.g., "10.000 HBD"), or a numeric value.
|
|
586
|
+
expiration (int, optional): Order lifetime in seconds; defaults to the node/configured order-expiration (typically 7 days).
|
|
587
|
+
killfill (bool, optional): If True, treat the order as fill-or-kill (cancel if not fully filled). Defaults to False.
|
|
588
|
+
account (str, optional): Account name placing the order. If omitted, the configured default_account is used. Raises ValueError if no account is available.
|
|
589
|
+
orderid (int, optional): Client-provided order identifier; a random 32-bit id is used if not supplied.
|
|
590
|
+
returnOrderId (bool or str, optional): If truthy (or set to "head"/"irreversible"), the call will wait according to the blocking mode and the returned transaction will include an "orderid" field.
|
|
591
|
+
|
|
592
|
+
Returns:
|
|
593
|
+
dict: The finalized transaction object returned by the blockchain finalizeOp call. If `returnOrderId` is used, the dict will include an "orderid" key.
|
|
594
|
+
|
|
595
|
+
Raises:
|
|
596
|
+
ValueError: If no account is provided or available from configuration.
|
|
597
|
+
AssertionError: If an Amount is provided whose asset symbol does not match the market's quote asset.
|
|
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
|
+
if isinstance(price, Price):
|
|
608
|
+
price = price.as_base(self["base"]["symbol"])
|
|
609
|
+
|
|
610
|
+
if isinstance(amount, Amount):
|
|
611
|
+
amount = Amount(amount, blockchain_instance=self.blockchain)
|
|
612
|
+
if not amount["asset"]["symbol"] == self["quote"]["symbol"]:
|
|
613
|
+
raise AssertionError(
|
|
614
|
+
"Price: {} does not match amount: {}".format(str(price), str(amount))
|
|
615
|
+
)
|
|
616
|
+
elif isinstance(amount, str):
|
|
617
|
+
amount = Amount(amount, blockchain_instance=self.blockchain)
|
|
618
|
+
else:
|
|
619
|
+
amount = Amount(amount, self["quote"]["symbol"], blockchain_instance=self.blockchain)
|
|
620
|
+
order = operations.Limit_order_create(
|
|
621
|
+
**{
|
|
622
|
+
"owner": account["name"],
|
|
623
|
+
"orderid": orderid or random.getrandbits(32),
|
|
624
|
+
"amount_to_sell": Amount(
|
|
625
|
+
float(amount),
|
|
626
|
+
self["quote"]["symbol"],
|
|
627
|
+
blockchain_instance=self.blockchain,
|
|
628
|
+
json_str=True,
|
|
629
|
+
),
|
|
630
|
+
"min_to_receive": Amount(
|
|
631
|
+
float(amount) * float(price),
|
|
632
|
+
self["base"]["symbol"],
|
|
633
|
+
blockchain_instance=self.blockchain,
|
|
634
|
+
json_str=True,
|
|
635
|
+
),
|
|
636
|
+
"expiration": formatTimeFromNow(expiration),
|
|
637
|
+
"fill_or_kill": killfill,
|
|
638
|
+
"prefix": self.blockchain.prefix,
|
|
639
|
+
"json_str": True,
|
|
640
|
+
}
|
|
641
|
+
)
|
|
642
|
+
if returnOrderId:
|
|
643
|
+
# Make blocking broadcasts
|
|
644
|
+
prevblocking = self.blockchain.blocking
|
|
645
|
+
self.blockchain.blocking = returnOrderId
|
|
646
|
+
|
|
647
|
+
tx = self.blockchain.finalizeOp(order, account["name"], "active")
|
|
648
|
+
|
|
649
|
+
if returnOrderId:
|
|
650
|
+
tx["orderid"] = tx["operation_results"][0][1]
|
|
651
|
+
self.blockchain.blocking = prevblocking
|
|
652
|
+
|
|
653
|
+
return tx
|
|
654
|
+
|
|
655
|
+
def cancel(
|
|
656
|
+
self,
|
|
657
|
+
orderNumbers: Union[int, List[int], Set[int], Tuple[int, ...]],
|
|
658
|
+
account: Optional[Union[str, Account]] = None,
|
|
659
|
+
**kwargs: Any,
|
|
660
|
+
) -> Dict[str, Any]:
|
|
661
|
+
"""Cancels an order you have placed in a given market. Requires
|
|
662
|
+
only the "orderNumbers".
|
|
663
|
+
|
|
664
|
+
:param orderNumbers: A single order number or a list of order numbers
|
|
665
|
+
:type orderNumbers: int, list
|
|
666
|
+
"""
|
|
667
|
+
if not account:
|
|
668
|
+
if "default_account" in self.blockchain.config:
|
|
669
|
+
account = self.blockchain.config["default_account"]
|
|
670
|
+
if not account:
|
|
671
|
+
raise ValueError("You need to provide an account")
|
|
672
|
+
account = Account(account, full=False, blockchain_instance=self.blockchain)
|
|
673
|
+
|
|
674
|
+
if not isinstance(orderNumbers, (list, set, tuple)):
|
|
675
|
+
orderNumbers = {orderNumbers}
|
|
676
|
+
|
|
677
|
+
op = []
|
|
678
|
+
for order in orderNumbers:
|
|
679
|
+
op.append(
|
|
680
|
+
operations.Limit_order_cancel(
|
|
681
|
+
**{"owner": account["name"], "orderid": order, "prefix": self.blockchain.prefix}
|
|
682
|
+
)
|
|
683
|
+
)
|
|
684
|
+
return self.blockchain.finalizeOp(op, account["name"], "active", **kwargs)
|
|
685
|
+
|
|
686
|
+
@staticmethod
|
|
687
|
+
def _weighted_average(
|
|
688
|
+
values: List[Union[int, float]], weights: List[Union[int, float]]
|
|
689
|
+
) -> float:
|
|
690
|
+
"""Calculates a weighted average"""
|
|
691
|
+
if not (len(values) == len(weights) and len(weights) > 0):
|
|
692
|
+
raise AssertionError("Length of both array must be the same and greater than zero!")
|
|
693
|
+
return sum(x * y for x, y in zip(values, weights)) / sum(weights)
|
|
694
|
+
|
|
695
|
+
@staticmethod
|
|
696
|
+
def btc_usd_ticker(verbose: bool = False) -> float:
|
|
697
|
+
"""
|
|
698
|
+
Return the market-weighted BTC/USD price aggregated from multiple external sources.
|
|
699
|
+
|
|
700
|
+
Queries a set of public endpoints (currently CoinGecko; legacy support for Bitfinex, GDAX, Kraken, OKCoin, Bitstamp is present)
|
|
701
|
+
and computes a volume-weighted average price (VWAP) across successful responses.
|
|
702
|
+
|
|
703
|
+
Parameters:
|
|
704
|
+
verbose (bool): If True, prints the raw price/volume map collected from each source.
|
|
705
|
+
|
|
706
|
+
Returns:
|
|
707
|
+
float: The VWAP of BTC in USD computed from available sources.
|
|
708
|
+
|
|
709
|
+
Raises:
|
|
710
|
+
RuntimeError: If no valid price data could be obtained from any source after several attempts.
|
|
711
|
+
"""
|
|
712
|
+
prices = {}
|
|
713
|
+
responses = []
|
|
714
|
+
urls = [
|
|
715
|
+
# "https://api.bitfinex.com/v1/pubticker/BTCUSD",
|
|
716
|
+
# "https://api.gdax.com/products/BTC-USD/ticker",
|
|
717
|
+
# "https://api.kraken.com/0/public/Ticker?pair=XBTUSD",
|
|
718
|
+
# "https://www.okcoin.com/api/v1/ticker.do?symbol=btc_usd",
|
|
719
|
+
# "https://www.bitstamp.net/api/v2/ticker/btcusd/",
|
|
720
|
+
"https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd&include_24hr_vol=true",
|
|
721
|
+
]
|
|
722
|
+
cnt = 0
|
|
723
|
+
while len(prices) == 0 and cnt < 5:
|
|
724
|
+
cnt += 1
|
|
725
|
+
try:
|
|
726
|
+
responses = list(httpx.get(u, timeout=30) for u in urls)
|
|
727
|
+
except Exception as e:
|
|
728
|
+
log.debug(str(e))
|
|
729
|
+
|
|
730
|
+
for r in [
|
|
731
|
+
x
|
|
732
|
+
for x in responses
|
|
733
|
+
if hasattr(x, "status_code") and x.status_code == 200 and x.json()
|
|
734
|
+
]:
|
|
735
|
+
try:
|
|
736
|
+
if "bitfinex" in str(r.url):
|
|
737
|
+
data = r.json()
|
|
738
|
+
prices["bitfinex"] = {
|
|
739
|
+
"price": float(data["last_price"]),
|
|
740
|
+
"volume": float(data["volume"]),
|
|
741
|
+
}
|
|
742
|
+
elif "gdax" in str(r.url):
|
|
743
|
+
data = r.json()
|
|
744
|
+
prices["gdax"] = {
|
|
745
|
+
"price": float(data["price"]),
|
|
746
|
+
"volume": float(data["volume"]),
|
|
747
|
+
}
|
|
748
|
+
elif "kraken" in str(r.url):
|
|
749
|
+
data = r.json()["result"]["XXBTZUSD"]["p"]
|
|
750
|
+
prices["kraken"] = {"price": float(data[0]), "volume": float(data[1])}
|
|
751
|
+
elif "okcoin" in str(r.url):
|
|
752
|
+
data = r.json()["ticker"]
|
|
753
|
+
prices["okcoin"] = {
|
|
754
|
+
"price": float(data["last"]),
|
|
755
|
+
"volume": float(data["vol"]),
|
|
756
|
+
}
|
|
757
|
+
elif "bitstamp" in str(r.url):
|
|
758
|
+
data = r.json()
|
|
759
|
+
prices["bitstamp"] = {
|
|
760
|
+
"price": float(data["last"]),
|
|
761
|
+
"volume": float(data["volume"]),
|
|
762
|
+
}
|
|
763
|
+
elif "coingecko" in str(r.url):
|
|
764
|
+
data = r.json()["bitcoin"]
|
|
765
|
+
if "usd_24h_vol" in data:
|
|
766
|
+
volume = float(data["usd_24h_vol"])
|
|
767
|
+
else:
|
|
768
|
+
volume = 1
|
|
769
|
+
prices["coingecko"] = {"price": float(data["usd"]), "volume": volume}
|
|
770
|
+
except KeyError as e:
|
|
771
|
+
log.info(str(e))
|
|
772
|
+
|
|
773
|
+
if verbose:
|
|
774
|
+
print(prices)
|
|
775
|
+
|
|
776
|
+
if len(prices) == 0:
|
|
777
|
+
raise RuntimeError("Obtaining BTC/USD prices has failed from all sources.")
|
|
778
|
+
|
|
779
|
+
# vwap
|
|
780
|
+
return Market._weighted_average(
|
|
781
|
+
[x["price"] for x in prices.values()], [x["volume"] for x in prices.values()]
|
|
782
|
+
)
|
|
783
|
+
|
|
784
|
+
@staticmethod
|
|
785
|
+
def hive_btc_ticker() -> float:
|
|
786
|
+
"""
|
|
787
|
+
Return the HIVE/BTC price as a volume-weighted average from multiple public exchanges.
|
|
788
|
+
|
|
789
|
+
Queries several public APIs (CoinGecko and others) to collect recent HIVE/BTC prices and 24h volumes, then computes a volume-weighted average price (VWAP). The function retries up to 5 times if no valid responses are obtained.
|
|
790
|
+
|
|
791
|
+
Returns:
|
|
792
|
+
float: VWAP price expressed in BTC per 1 HIVE.
|
|
793
|
+
|
|
794
|
+
Raises:
|
|
795
|
+
RuntimeError: If no valid price data could be obtained from any source.
|
|
796
|
+
"""
|
|
797
|
+
prices = {}
|
|
798
|
+
responses = []
|
|
799
|
+
urls = [
|
|
800
|
+
# "https://bittrex.com/api/v1.1/public/getmarketsummary?market=BTC-HIVE",
|
|
801
|
+
# "https://api.binance.com/api/v1/ticker/24hr",
|
|
802
|
+
# "https://api.probit.com/api/exchange/v1/ticker?market_ids=HIVE-USDT",
|
|
803
|
+
"https://api.coingecko.com/api/v3/simple/price?ids=hive&vs_currencies=btc&include_24hr_vol=true",
|
|
804
|
+
]
|
|
805
|
+
headers = {
|
|
806
|
+
"Content-type": "application/x-www-form-urlencoded",
|
|
807
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.71 Safari/537.36",
|
|
808
|
+
}
|
|
809
|
+
cnt = 0
|
|
810
|
+
while len(prices) == 0 and cnt < 5:
|
|
811
|
+
cnt += 1
|
|
812
|
+
try:
|
|
813
|
+
responses = list(httpx.get(u, headers=headers, timeout=30) for u in urls)
|
|
814
|
+
except Exception as e:
|
|
815
|
+
log.debug(str(e))
|
|
816
|
+
|
|
817
|
+
for r in [
|
|
818
|
+
x
|
|
819
|
+
for x in responses
|
|
820
|
+
if hasattr(x, "status_code") and x.status_code == 200 and x.json()
|
|
821
|
+
]:
|
|
822
|
+
try:
|
|
823
|
+
if "poloniex" in str(r.url):
|
|
824
|
+
data = r.json()["BTC_HIVE"]
|
|
825
|
+
prices["poloniex"] = {
|
|
826
|
+
"price": float(data["last"]),
|
|
827
|
+
"volume": float(data["baseVolume"]),
|
|
828
|
+
}
|
|
829
|
+
elif "bittrex" in str(r.url):
|
|
830
|
+
data = r.json()["result"][0]
|
|
831
|
+
price = (data["Bid"] + data["Ask"]) / 2
|
|
832
|
+
prices["bittrex"] = {"price": price, "volume": data["BaseVolume"]}
|
|
833
|
+
elif "binance" in str(r.url):
|
|
834
|
+
data = [x for x in r.json() if x["symbol"] == "HIVEBTC"][0]
|
|
835
|
+
prices["binance"] = {
|
|
836
|
+
"price": float(data["lastPrice"]),
|
|
837
|
+
"volume": float(data["quoteVolume"]),
|
|
838
|
+
}
|
|
839
|
+
elif "huobi" in str(r.url):
|
|
840
|
+
data = r.json()["data"][-1]
|
|
841
|
+
prices["huobi"] = {
|
|
842
|
+
"price": float(data["close"]),
|
|
843
|
+
"volume": float(data["vol"]),
|
|
844
|
+
}
|
|
845
|
+
elif "upbit" in str(r.url):
|
|
846
|
+
data = r.json()[-1]
|
|
847
|
+
prices["upbit"] = {
|
|
848
|
+
"price": float(data["tradePrice"]),
|
|
849
|
+
"volume": float(data["tradeVolume"]),
|
|
850
|
+
}
|
|
851
|
+
elif "probit" in str(r.url):
|
|
852
|
+
data = r.json()["data"]
|
|
853
|
+
prices["probit"] = {
|
|
854
|
+
"price": float(data["last"]),
|
|
855
|
+
"volume": float(data["base_volume"]),
|
|
856
|
+
}
|
|
857
|
+
elif "coingecko" in str(r.url):
|
|
858
|
+
data = r.json()["hive"]
|
|
859
|
+
if "btc_24h_vol" in data:
|
|
860
|
+
volume = float(data["btc_24h_vol"])
|
|
861
|
+
else:
|
|
862
|
+
volume = 1
|
|
863
|
+
prices["coingecko"] = {"price": float(data["btc"]), "volume": volume}
|
|
864
|
+
except KeyError as e:
|
|
865
|
+
log.info(str(e))
|
|
866
|
+
|
|
867
|
+
if len(prices) == 0:
|
|
868
|
+
raise RuntimeError("Obtaining HIVE/BTC prices has failed from all sources.")
|
|
869
|
+
|
|
870
|
+
return Market._weighted_average(
|
|
871
|
+
[x["price"] for x in prices.values()], [x["volume"] for x in prices.values()]
|
|
872
|
+
)
|
|
873
|
+
|
|
874
|
+
def hive_usd_implied(self) -> float:
|
|
875
|
+
"""Returns the current HIVE/USD market price"""
|
|
876
|
+
return self.hive_btc_ticker() * self.btc_usd_ticker()
|