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.
Files changed (87) hide show
  1. hive_nectar-0.2.9.dist-info/METADATA +194 -0
  2. hive_nectar-0.2.9.dist-info/RECORD +87 -0
  3. hive_nectar-0.2.9.dist-info/WHEEL +4 -0
  4. hive_nectar-0.2.9.dist-info/entry_points.txt +2 -0
  5. hive_nectar-0.2.9.dist-info/licenses/LICENSE.txt +23 -0
  6. nectar/__init__.py +37 -0
  7. nectar/account.py +5076 -0
  8. nectar/amount.py +553 -0
  9. nectar/asciichart.py +303 -0
  10. nectar/asset.py +122 -0
  11. nectar/block.py +574 -0
  12. nectar/blockchain.py +1242 -0
  13. nectar/blockchaininstance.py +2590 -0
  14. nectar/blockchainobject.py +263 -0
  15. nectar/cli.py +5937 -0
  16. nectar/comment.py +1552 -0
  17. nectar/community.py +854 -0
  18. nectar/constants.py +95 -0
  19. nectar/discussions.py +1437 -0
  20. nectar/exceptions.py +152 -0
  21. nectar/haf.py +381 -0
  22. nectar/hive.py +630 -0
  23. nectar/imageuploader.py +114 -0
  24. nectar/instance.py +113 -0
  25. nectar/market.py +876 -0
  26. nectar/memo.py +542 -0
  27. nectar/message.py +379 -0
  28. nectar/nodelist.py +309 -0
  29. nectar/price.py +603 -0
  30. nectar/profile.py +74 -0
  31. nectar/py.typed +0 -0
  32. nectar/rc.py +333 -0
  33. nectar/snapshot.py +1024 -0
  34. nectar/storage.py +62 -0
  35. nectar/transactionbuilder.py +659 -0
  36. nectar/utils.py +630 -0
  37. nectar/version.py +3 -0
  38. nectar/vote.py +722 -0
  39. nectar/wallet.py +472 -0
  40. nectar/witness.py +728 -0
  41. nectarapi/__init__.py +12 -0
  42. nectarapi/exceptions.py +126 -0
  43. nectarapi/graphenerpc.py +596 -0
  44. nectarapi/node.py +194 -0
  45. nectarapi/noderpc.py +79 -0
  46. nectarapi/openapi.py +107 -0
  47. nectarapi/py.typed +0 -0
  48. nectarapi/rpcutils.py +98 -0
  49. nectarapi/version.py +3 -0
  50. nectarbase/__init__.py +15 -0
  51. nectarbase/ledgertransactions.py +106 -0
  52. nectarbase/memo.py +242 -0
  53. nectarbase/objects.py +521 -0
  54. nectarbase/objecttypes.py +21 -0
  55. nectarbase/operationids.py +102 -0
  56. nectarbase/operations.py +1357 -0
  57. nectarbase/py.typed +0 -0
  58. nectarbase/signedtransactions.py +89 -0
  59. nectarbase/transactions.py +11 -0
  60. nectarbase/version.py +3 -0
  61. nectargraphenebase/__init__.py +27 -0
  62. nectargraphenebase/account.py +1121 -0
  63. nectargraphenebase/aes.py +49 -0
  64. nectargraphenebase/base58.py +197 -0
  65. nectargraphenebase/bip32.py +575 -0
  66. nectargraphenebase/bip38.py +110 -0
  67. nectargraphenebase/chains.py +15 -0
  68. nectargraphenebase/dictionary.py +2 -0
  69. nectargraphenebase/ecdsasig.py +309 -0
  70. nectargraphenebase/objects.py +130 -0
  71. nectargraphenebase/objecttypes.py +8 -0
  72. nectargraphenebase/operationids.py +5 -0
  73. nectargraphenebase/operations.py +25 -0
  74. nectargraphenebase/prefix.py +13 -0
  75. nectargraphenebase/py.typed +0 -0
  76. nectargraphenebase/signedtransactions.py +221 -0
  77. nectargraphenebase/types.py +557 -0
  78. nectargraphenebase/unsignedtransactions.py +288 -0
  79. nectargraphenebase/version.py +3 -0
  80. nectarstorage/__init__.py +57 -0
  81. nectarstorage/base.py +317 -0
  82. nectarstorage/exceptions.py +15 -0
  83. nectarstorage/interfaces.py +244 -0
  84. nectarstorage/masterpassword.py +237 -0
  85. nectarstorage/py.typed +0 -0
  86. nectarstorage/ram.py +27 -0
  87. 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()