ostium-python-sdk 0.1.3__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.
@@ -0,0 +1,604 @@
1
+ from datetime import datetime
2
+ from decimal import Decimal
3
+ import os
4
+ from humanize import naturaltime
5
+ from web3 import Web3
6
+ from ast import literal_eval
7
+
8
+ from .constants import MAX_PROFIT_P, MAX_STOP_LOSS_P
9
+ from .formulae import GetTakeProfitPrice
10
+
11
+
12
+ def send_slack_message(client, from_username, message):
13
+ # Send a message
14
+ if client is not None:
15
+ message = f'{from_username}: {message}'
16
+ client.chat_postMessage(
17
+ channel="telegram-bot",
18
+ text=message,
19
+ username="User Feedback"
20
+ )
21
+ else:
22
+ print(
23
+ f'********* client is None, from_username: {from_username}, message: {message}')
24
+
25
+
26
+ def get_asset_group_name(from_asset, to_asset):
27
+ if from_asset in ['BTC', 'ETH', 'SOL']:
28
+ return 'crypto'
29
+ elif from_asset in ['HG', 'CL', 'XAU', 'XAG', 'XPD', 'XPT', 'NG', 'LCO']:
30
+ return 'commodities'
31
+ elif from_asset in ['SPX', 'HSI', 'NIK', 'FTS', 'DAX']:
32
+ return 'indices'
33
+ elif from_asset in ['EUR', 'GBP'] or (from_asset == 'USD' and to_asset == 'JPY'):
34
+ return 'forex'
35
+ else:
36
+ print('------> get_asset_group_name need to map asset asset_group_name',
37
+ from_asset, to_asset)
38
+ return ''
39
+
40
+ #
41
+ # rename market_price as open_price. Pass Leverage.
42
+ # Get the None value here returned by calling GetTakeProfitPrice
43
+ # and consolidate the logic of these 2 functions to one
44
+ #
45
+
46
+
47
+ def get_asset_change_emoji(asset_group_name, change, period_hours):
48
+ """
49
+ Returns an emoji indicating price movement severity based on asset type and time period.
50
+ Different assets have different volatility profiles:
51
+ - Crypto: More volatile but realistic (2% in 24h is notable, 100% in 1y is extreme)
52
+ - Commodities: Medium volatility
53
+ - Forex/Indices: Lower volatility (tighter thresholds)
54
+ """
55
+ # Define thresholds for each period and asset group (values in percentages)
56
+ thresholds = {
57
+ 'crypto': {
58
+ 1: [0.3, 0.7, 1.2, 2, 3], # 1h
59
+ 8: [0.7, 1.5, 2.5, 4, 6], # 8h
60
+ 24: [1, 2, 4, 6, 8], # 24h
61
+ 365*24: [10, 25, 50, 75, 100] # 1y: More realistic yearly moves
62
+ },
63
+ 'commodities': {
64
+ 1: [0.2, 0.5, 1, 2, 3], # 1h
65
+ 8: [0.5, 1, 2, 3, 5], # 8h
66
+ 24: [1, 2, 3, 5, 7], # 24h
67
+ 365*24: [10, 20, 30, 50, 70] # 1y
68
+ },
69
+ 'forex': {
70
+ 1: [0.1, 0.2, 0.3, 0.5, 1], # 1h
71
+ 8: [0.2, 0.4, 0.6, 1, 1.5], # 8h
72
+ 24: [0.3, 0.6, 1, 1.5, 2], # 24h
73
+ 365*24: [5, 10, 15, 20, 25] # 1y
74
+ },
75
+ 'indices': {
76
+ 1: [0.2, 0.4, 0.6, 1, 1.5], # 1h
77
+ 8: [0.4, 0.8, 1.2, 2, 3], # 8h
78
+ 24: [0.8, 1.5, 2.5, 3.5, 5], # 24h
79
+ 365*24: [8, 15, 25, 35, 45] # 1y
80
+ }
81
+ }
82
+
83
+ if period_hours not in thresholds[asset_group_name]:
84
+ return ''
85
+
86
+ # Get absolute change for comparison
87
+ abs_change = abs(change)
88
+ period_thresholds = thresholds[asset_group_name][period_hours]
89
+
90
+ # Determine severity level
91
+ if abs_change < period_thresholds[0]:
92
+ emoji = '' # Minimal change
93
+ elif abs_change < period_thresholds[1]:
94
+ emoji = '📈' if change > 0 else '📉'
95
+ elif abs_change < period_thresholds[2]:
96
+ emoji = '⬆️' if change > 0 else '⬇️'
97
+ elif abs_change < period_thresholds[3]:
98
+ emoji = '🔥' if change > 0 else '💧'
99
+ elif abs_change < period_thresholds[4]:
100
+ emoji = '🚀' if change > 0 else '🌪'
101
+ else:
102
+ emoji = '⚡️' if change > 0 else '💀'
103
+
104
+ return f' {emoji}'
105
+
106
+
107
+ def get_tp_sl_min_max_allowed_values(is_long: bool, market_price: Decimal, leverage: Decimal, is_tp: bool) -> tuple[Decimal, Decimal]:
108
+ """
109
+ Returns (min_allowed, max_allowed) tuple for take profit or stop loss prices based on position direction
110
+
111
+ Args:
112
+ is_long: True if long position, False if short
113
+ market_price: Current market price
114
+ is_tp: True if checking take profit, False if checking stop loss
115
+ """
116
+
117
+ tp_or_sl_price = GetTakeProfitPrice(
118
+ is_tp, market_price, leverage, is_long, MAX_PROFIT_P if is_tp else MAX_STOP_LOSS_P)
119
+ if is_tp:
120
+ # Take profit must be above market for longs, below for shorts
121
+ return (
122
+ (Decimal(market_price), tp_or_sl_price) if is_long else
123
+ (tp_or_sl_price, Decimal(market_price))
124
+ )
125
+ else:
126
+ # Stop loss must be below market for longs, above for shorts
127
+ return (
128
+ (tp_or_sl_price, Decimal(market_price)) if is_long else
129
+ (Decimal(market_price), tp_or_sl_price)
130
+ )
131
+
132
+
133
+ def is_valid_evm_address(address):
134
+ is_valid = Web3.is_address(address)
135
+ print('----->is_valid_evm_address called with',
136
+ address, 'and returns', is_valid)
137
+ return is_valid
138
+
139
+ # returns (is_valid, percentage_inputted or None is not relevant, not relevant is is_valid is False)
140
+
141
+
142
+ def is_valid_input(text, validation):
143
+
144
+ if validation is None:
145
+ return True, None
146
+
147
+ if 'is_address_and_none_self' in validation and validation['is_address_and_none_self']:
148
+ trader_address = validation['own_address']
149
+ return is_valid_evm_address(text) and (text).lower() != trader_address.lower(), None
150
+
151
+ # Check if it's a valid decimal number
152
+ if not is_valid_decimal(text):
153
+ if 'percentage_allowed_for' not in validation:
154
+ return False, None
155
+ else:
156
+ # replace only 1 occurrence of '%' so 5%% isn't valid
157
+ text = text.replace('%', '', 1)
158
+ if not is_valid_decimal(text) or Decimal(text) == 0: # cant accept 0%
159
+ return False, None
160
+ else:
161
+ return True, Decimal(text)
162
+
163
+ # By now, text is not specified as a percentage
164
+
165
+ # Convert to Decimal for comparison
166
+ value = Decimal(text)
167
+
168
+ # Special case: if zero_to_remove is present and value is 0
169
+ if 'zero_to_remove' in validation and validation['zero_to_remove'] and value == 0:
170
+ return True, False
171
+
172
+ if ('zero_to_remove' not in validation or not validation['zero_to_remove']) and value == 0:
173
+ return False, False
174
+
175
+ # Check minimum value if specified
176
+ if 'min' in validation and value < Decimal(validation['min']):
177
+ return False, False
178
+
179
+ # Check maximum value if specified
180
+ if 'max' in validation and value > Decimal(validation['max']):
181
+ return False, False
182
+
183
+ return True, False
184
+
185
+
186
+ def get_period_hours_text(period_hours):
187
+ if period_hours == 1:
188
+ return '1h'
189
+ elif period_hours == 8:
190
+ return '8h'
191
+ elif period_hours == 24:
192
+ return '24h'
193
+ elif period_hours == 365*24:
194
+ return '1y'
195
+
196
+
197
+ def build_asset_callback_data(asset_id, period_hours=None):
198
+ return f'chooseAsset|{asset_id}|{period_hours if period_hours else "24"}'
199
+
200
+ # period_hours is 1, 8, 24, 8760 (365*24)
201
+
202
+
203
+ def parse_performances(performances, period_hours):
204
+ try:
205
+ if performances:
206
+ if period_hours == 24:
207
+ low = Decimal(performances['low24h'])
208
+ high = Decimal(performances['high24h'])
209
+ change = Decimal(performances['24hChange'])
210
+ elif period_hours == 8:
211
+ low = Decimal(performances['low8h'])
212
+ high = Decimal(performances['high8h'])
213
+ change = Decimal(performances['8hChange'])
214
+ elif period_hours == 1:
215
+ low = Decimal(performances['low1h'])
216
+ high = Decimal(performances['high1h'])
217
+ change = Decimal(performances['1hChange'])
218
+ elif period_hours == 365*24:
219
+ low = Decimal(performances['low1y']
220
+ ) if 'low1y' in performances else None
221
+ high = Decimal(performances['high1y']
222
+ ) if 'high1y' in performances else None
223
+ change = Decimal(
224
+ performances['1yChange']) if '1yChange' in performances else None
225
+ else:
226
+ low = None
227
+ high = None
228
+ change = None
229
+
230
+ return low, high, change
231
+ else:
232
+ return Decimal(0), Decimal(0), Decimal(0)
233
+
234
+ except Exception as e:
235
+ return Decimal(0), Decimal(0), Decimal(0)
236
+
237
+
238
+ def get_tp_sl_prices(trade_params):
239
+ tp_price = 0
240
+ sl_price = 0
241
+
242
+ if 'tp' in trade_params and str(trade_params['tp']) != '0':
243
+ tp_price = float(trade_params['tp'])
244
+
245
+ if 'sl' in trade_params and str(trade_params['sl']) != '0':
246
+ sl_price = float(trade_params['sl'])
247
+
248
+ return tp_price, sl_price
249
+
250
+
251
+ def get_oi_usage_emoji(oi, max_oi_cap):
252
+ oi_percent = oi/max_oi_cap*100
253
+ if oi_percent >= 100:
254
+ return '🚫'
255
+ elif oi_percent > 80:
256
+ return '🔥🔥🔥'
257
+ elif oi_percent > 50:
258
+ return '🔥🔥'
259
+ elif oi_percent > 40:
260
+ return '🔥'
261
+ else:
262
+ return ''
263
+
264
+
265
+ def get_validation_text(validation_dict):
266
+ if 'min' in validation_dict and 'max' in validation_dict:
267
+ return f"between {validation_dict['min']} and {validation_dict['max']}"
268
+ else:
269
+ if 'min' in validation_dict and 'max' not in validation_dict:
270
+ return f"equal or above {validation_dict['min']}"
271
+ elif 'max' in validation_dict and 'min' not in validation_dict:
272
+ return f"equal or below {validation_dict['max']}"
273
+ else:
274
+ return ''
275
+
276
+
277
+ def format_awaiting_input_text(text, validation_dict, market_mid_price=None):
278
+ if 'min' in validation_dict and 'max' in validation_dict:
279
+ from_to_str = f"range:\n{validation_dict['min']} - {validation_dict['max']}"
280
+ else:
281
+ if 'min' in validation_dict and 'max' not in validation_dict:
282
+ from_to_str = f"min. {validation_dict['min']}"
283
+ elif 'max' in validation_dict and 'min' not in validation_dict:
284
+ from_to_str = f"max. {validation_dict['max']}"
285
+ else:
286
+ from_to_str = ''
287
+
288
+ note = f"\nOr 0 to remove" if 'zero_to_remove' in validation_dict and validation_dict[
289
+ 'zero_to_remove'] else ''
290
+
291
+ tp_sl_examples = ""
292
+ if 'percentage_allowed_for' in validation_dict:
293
+ purpose = validation_dict['percentage_allowed_for']
294
+ percentages = [25, 50, 75, 100, 500,
295
+ 900] if purpose == "tp" else [5, 10, 25, 50, 85]
296
+
297
+ tp_sl_examples = f"\n\nOr specify {purpose.title()} as a percentage, i.e:\n"
298
+ for percent in percentages:
299
+ price = GetTakeProfitPrice(
300
+ is_tp=(purpose == "tp"),
301
+ open_price=validation_dict['metadata']['open_price'],
302
+ leverage=validation_dict['metadata']['leverage'],
303
+ long=validation_dict['metadata']['is_long'],
304
+ profit_p=percent
305
+ )
306
+ tp_sl_examples += f"{percent}% 👉 ${format_with_precision(price, precision=5)}\n"
307
+
308
+ return f'{text}\n\n{from_to_str}{tp_sl_examples}{note}'
309
+
310
+
311
+ def get_profit_emoji(profit_percent):
312
+ if profit_percent >= 80:
313
+ return '💰💰'
314
+ elif profit_percent >= 60:
315
+ return '💰'
316
+ elif profit_percent >= 50:
317
+ return '🚀'
318
+ elif profit_percent >= 20:
319
+ return '🔥'
320
+ elif profit_percent > -3:
321
+ return ''
322
+ elif profit_percent >= -5:
323
+ return '🤨'
324
+ elif profit_percent >= -20:
325
+ return '😟'
326
+ elif profit_percent >= -25:
327
+ return '😲'
328
+ elif profit_percent >= -30:
329
+ return '😢'
330
+ elif profit_percent >= -40:
331
+ return '😱'
332
+ elif profit_percent >= -50:
333
+ return '💣'
334
+ else:
335
+ return '💀' # Skull for severe losses
336
+
337
+
338
+ def get_oi_in_usd(oi, price):
339
+ return format_with_precision(Web3.from_wei(int(oi) * price, 'ether'), precision=2)
340
+
341
+
342
+ def get_oi_state(pair_details, mid_price):
343
+ long_oi = get_oi_in_usd(pair_details['longOI'], mid_price)
344
+ short_oi = get_oi_in_usd(pair_details['shortOI'], mid_price)
345
+
346
+ max_oi_cap = format_with_precision(Web3.from_wei(
347
+ int(pair_details['maxOI']), 'mwei'), precision=2)
348
+
349
+ return Decimal(long_oi), Decimal(short_oi), Decimal(max_oi_cap)
350
+
351
+
352
+ def format_with_precision(number, precision):
353
+ """
354
+ Formats a number to a specified decimal precision, removing trailing zeros.
355
+
356
+ Args:
357
+ number: The number to be formatted (can be int, float, or numeric string)
358
+ precision (int): Maximum number of decimal places to round to
359
+
360
+ Returns:
361
+ str: Formatted number with up to specified decimal precision, trailing zeros removed
362
+ """
363
+ try:
364
+ if callable(number):
365
+ raise TypeError("Input cannot be a function")
366
+
367
+ float_number = float(number)
368
+ precision = int(precision)
369
+
370
+ # Format with specified precision first
371
+ formatted = "{:.{}f}".format(round(float_number, precision), precision)
372
+ # Remove trailing zeros after decimal point, but keep at least one digit before decimal
373
+ formatted = formatted.rstrip('0').rstrip(
374
+ '.') if '.' in formatted else formatted
375
+
376
+ return float(formatted)
377
+
378
+ except (TypeError, ValueError) as e:
379
+ raise TypeError(f"Invalid input: {e}")
380
+
381
+ # my_time is datetime.fromtimestamp
382
+
383
+
384
+ def format_time_ago(my_time):
385
+ ago = naturaltime(datetime.now() - my_time).replace('minutes',
386
+ 'm').replace('seconds', 's').replace('hours', 'h').replace('days', 'd')
387
+ return f'{my_time.strftime("%H:%M %a %b %-d")} ({ago})'
388
+
389
+
390
+ def get_order_details(order_details):
391
+ open_price = Web3.from_wei(int(order_details['openPrice']), 'ether')
392
+
393
+ limit_order_created_time = datetime.fromtimestamp(
394
+ int(order_details['initiatedAt']))
395
+
396
+ leverage = Web3.from_wei(int(order_details['leverage']), 'kwei')*10
397
+ collateral = Web3.from_wei(int(order_details['collateral']), 'mwei')
398
+ is_long = order_details['isBuy']
399
+ limit_type = order_details['limitType']
400
+ pairIndex, index = parse_limit_order_id(order_details['id'])
401
+
402
+ sl_price = Web3.from_wei(int(order_details['stopLossPrice']), 'ether')
403
+ tp_price = Web3.from_wei(int(order_details['takeProfitPrice']), 'ether')
404
+
405
+ atLeastTradeNotional = 0
406
+ if open_price:
407
+ atLeastTradeNotional = collateral*leverage/open_price
408
+
409
+ return limit_type, sl_price, tp_price, open_price, leverage, limit_order_created_time, collateral, pairIndex, index, is_long, atLeastTradeNotional
410
+
411
+
412
+ def get_trade_details(trade_details):
413
+ # print('\n\nget_trade_details called with trade_details\n\n', trade_details, '\n\n')
414
+ open_price = Web3.from_wei(int(trade_details['openPrice']), 'ether')
415
+ sl_price = Web3.from_wei(int(trade_details['stopLossPrice']), 'ether')
416
+ tp_price = Web3.from_wei(int(trade_details['takeProfitPrice']), 'ether')
417
+ tradeNotional = Web3.from_wei(int(trade_details['tradeNotional']), 'ether')
418
+ trans_time = datetime.fromtimestamp(int(trade_details['timestamp']))
419
+ leverage = Web3.from_wei(int(trade_details['leverage']), 'kwei')*10
420
+ collateral = Web3.from_wei(int(trade_details['collateral']), 'mwei')
421
+ is_long = trade_details['isBuy']
422
+ pairIndex = trade_details['pair']['id']
423
+ index = trade_details['index']
424
+
425
+ return open_price, round(tradeNotional, 18), trans_time, leverage, collateral, pairIndex, index, is_long, sl_price, tp_price
426
+
427
+
428
+ def parse_limit_order_id(limit_order_id):
429
+ # 0x3750a14869d419f1069cbf7cbe47a89b2dc1d4c4_0_0
430
+ trader, pairIndex, index = limit_order_id.split('_')
431
+
432
+ return pairIndex, index
433
+
434
+
435
+ def is_numeric(value):
436
+ try:
437
+ float(value)
438
+ return True
439
+ except ValueError:
440
+ return False
441
+
442
+
443
+ def fromErrorCodeToMessage(error_code):
444
+ # ----->fromErrorCodeToMessage(error_code) called with ('execution reverted: ERC20: transfer amount exceeds balance', '0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002645524332303a207472616e7366657220616d6f756e7420657863656564732062616c616e63650000000000000000000000000000000000000000000000000000')
445
+ # ----->Did't find the error, so returning - fromErrorCodeToMessage(error_code) returns Unknown error (missing in error_map?)
446
+
447
+ print('----->fromErrorCodeToMessage(error_code) called with', str(error_code))
448
+
449
+ # Create reverse mapping of hash -> error name
450
+ error_map = {
451
+ "80a71fc5": "AboveMaxAllowedCollateral()",
452
+ "f77a8069": "AlreadyMarketClosed(address,uint16,uint8)",
453
+ "eca695e1": "BelowMinLevPos()",
454
+ "5be5878a": "DelegatedActionFailed()",
455
+ "46c4ede2": "ExposureLimits()",
456
+ "4f285592": "IsContract(address)",
457
+ "084986e7": "IsDone()",
458
+ "1309a563": "IsPaused()",
459
+ "5c12ea62": "MaxPendingMarketOrdersReached(address)",
460
+ "e6f47fab": "MaxTradesPerPairReached(address,uint16)",
461
+ "2a917859": "NoDelegate(address)",
462
+ "a35ee470": "NoLimitFound(address,uint16,uint8)",
463
+ "17e08e97": "NoTradeFound(address,uint16,uint8)",
464
+ "efa9e5be": "NoTradeToTimeoutFound(uint256)",
465
+ "c7fe4d00": "NotCloseMarketTimeoutOrder(uint256)",
466
+ "502b946d": "NotDelegate(address,address)",
467
+ "093650d5": "NotGov(address)",
468
+ "1add0915": "NotOpenMarketTimeoutOrder(uint256)",
469
+ "432b6c83": "NotTradesUpKeep(address)",
470
+ "df17e316": "NotWhitelisted(address)",
471
+ "5ac89f62": "NotYourOrder(uint256,address)",
472
+ "f3d0b126": "NullAddr()",
473
+ "cb87b762": "PairNotListed(uint16)",
474
+ "dd9397bb": "TriggerPending(address,uint16,uint8)",
475
+ "3e0b1869": "WaitTimeout(uint256)",
476
+ "35fe85c5": "WrongLeverage(uint32)",
477
+ "5863f789": "WrongParams()",
478
+ "083fbd78": "WrongSL()",
479
+ "a41bb918": "WrongTP()"
480
+ }
481
+
482
+ # Search for any of the known error hashes within the error_code string
483
+ for hash_code, error_message in error_map.items():
484
+ if hash_code in str(error_code):
485
+ ret = error_message
486
+ print('----->fromErrorCodeToMessage(error_code) returns', ret)
487
+ return str(ret), None
488
+
489
+ # If we couldn't find the error in error_map, try to parse the error
490
+ try:
491
+ # Convert string representation to dictionary if needed
492
+ error_dict = error_code if isinstance(
493
+ error_code, dict) else literal_eval(str(error_code))
494
+ if isinstance(error_dict, dict) and 'message' in error_dict:
495
+ if 'insufficient funds for gas * price + value' in error_dict['message']:
496
+ suggestion = 'Please top up your account with more ETH'
497
+ return error_dict["message"], suggestion
498
+ else:
499
+ return error_dict['message'], None
500
+ except (ValueError, SyntaxError):
501
+ pass
502
+
503
+ if 'execution reverted: ERC20: transfer amount exceeds balance' in str(error_code):
504
+ suggestion = 'Please top up your account with more USDC'
505
+ return 'execution reverted: ERC20: transfer amount exceeds balance', suggestion
506
+
507
+ ret = 'Unknown error (missing in error_map?)'
508
+ print('----->Did\'t find the error, so returning - fromErrorCodeToMessage(error_code) returns', ret)
509
+ return str(ret), None
510
+
511
+
512
+ # def is_production():
513
+ # return 'sepolia' not in (os.getenv('RPC_PROVIDER')).lower()
514
+
515
+
516
+ def get_arbiscan_transaction_url(transaction_hash):
517
+ if (is_production()):
518
+ return f'https://arbiscan.io/tx/0x{transaction_hash}'
519
+ else:
520
+ return f'https://sepolia.arbiscan.io/tx/0x{transaction_hash}'
521
+
522
+
523
+ def get_max_leverage_and_min_leverage_position(pair_details):
524
+ max_leverage = int(pair_details['maxLeverage']) / 100
525
+ min_leverage_position = int(pair_details['fee']['minLevPos']) / 10 ** 6
526
+
527
+ if (max_leverage == 0):
528
+ max_leverage = int(pair_details['group']['maxLeverage']) / 100
529
+
530
+ return int(max_leverage), int(min_leverage_position)
531
+
532
+
533
+ def to_base_units(amount: float, decimals: int = 6) -> int:
534
+ """
535
+ Converts a decimal number to base units by multiplying by 10^decimals
536
+
537
+ Args:
538
+ amount (float): The amount to convert (e.g., 1.23)
539
+ decimals (int, optional): Number of decimal places. Defaults to 6 for USDC.
540
+
541
+ Returns:
542
+ int: The amount in base units (e.g., 1.23 -> 1230000 for decimals=6)
543
+ """
544
+ return int(float(amount) * 10**decimals)
545
+
546
+
547
+ def convert_to_scaled_integer(value, precision=5, scale=18):
548
+ # First scale to the precision we want to preserve (e.g., 5 decimal places)
549
+ precise_value = round(Decimal(value) * (10 ** precision))
550
+ # Then pad with zeros to reach 18 decimals
551
+ scaled_value = precise_value * (10 ** (scale - precision))
552
+ return scaled_value
553
+
554
+
555
+ def is_valid_decimal(s, must_be_positive=True):
556
+ try:
557
+ float(s)
558
+ except ValueError:
559
+ return False
560
+ else:
561
+ if must_be_positive and float(s) < 0:
562
+ return False
563
+ return True
564
+
565
+
566
+ def format_available_balance(balance_of_ether, usdc_balance):
567
+ eth_warning = ' ✖️ <i>gas</i>' if Decimal(
568
+ balance_of_ether) < Decimal('0.00015') else ''
569
+ return f'Available: {format_with_precision(usdc_balance, precision=2)} USDC, {format_with_precision(balance_of_ether, precision=5)} ETH{eth_warning}'
570
+
571
+
572
+ def format_current_portfolio(total_open_trades_net_value, usdc_balance):
573
+ return f'Wallet worth: <b>{format_with_precision(Decimal(total_open_trades_net_value) + Decimal(usdc_balance), precision=2)} USDC</b>'
574
+
575
+
576
+ def get_asset_name(from_asset, to_asset):
577
+ asset = f'{from_asset}/{to_asset}'
578
+ switch = {
579
+ 'BTC/USD': 'Bitcoin',
580
+ 'ETH/USD': 'Ethereum',
581
+ 'SOL/USD': 'Solana',
582
+ 'XAU/USD': 'Gold',
583
+ 'XAG/USD': 'Silver',
584
+ 'EUR/USD': 'Euro',
585
+ 'GBP/USD': 'Pound',
586
+ 'USD/JPY': 'Yen',
587
+ 'CL/USD': 'Crude Oil',
588
+ 'NG/USD': 'Natural Gas',
589
+ 'HG/USD': 'Copper',
590
+ 'SPX/USD': 'S&P 500',
591
+ }
592
+ return switch.get(asset, asset)
593
+
594
+
595
+ def convert_decimals(obj):
596
+ if isinstance(obj, dict):
597
+ return {key: convert_decimals(value) for key, value in obj.items()}
598
+ elif isinstance(obj, list):
599
+ return [convert_decimals(item) for item in obj]
600
+ elif isinstance(obj, Decimal):
601
+ return str(obj) # or float(obj) if you prefer
602
+ return obj
603
+
604
+ # timestamp is a string in seconds as returned from graph
@@ -0,0 +1,93 @@
1
+ Metadata-Version: 2.1
2
+ Name: ostium-python-sdk
3
+ Version: 0.1.3
4
+ Summary: A python based SDK developed for interacting with Ostium, a leveraged trading application for trading currencies, commodities, indices, crypto and more.
5
+ Home-page: https://github.com/0xOstium/ostium-python-sdk
6
+ Author: ami@ostium.io
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.8
12
+ Requires-Python: >=3.8
13
+ Description-Content-Type: text/markdown
14
+ Requires-Dist: web3>=6.0.0
15
+
16
+
17
+ # Ostium Python SDK
18
+
19
+ A python based SDK developed for interacting with Ostium v1 Trading Platform (https://ostium.app/)
20
+
21
+ Ostium is a decentralized perpetuals exchange on Arbitrum (Ethereum L2) with a focus on providing a seamless experience for traders for trading currencies, commodities, indices, crypto and more.
22
+
23
+ This SDK is designed to be used by developers who want to build applications on top of Ostium and automate their trading strategies.
24
+
25
+
26
+ ## Pip Install
27
+
28
+ The SDK can be installed via pip:
29
+
30
+ ```bash
31
+ pip install ostium-python-sdk
32
+ ```
33
+
34
+ ## Requirements
35
+
36
+ Developed using:
37
+ ```python
38
+ python=3.8
39
+ ```
40
+
41
+ ## Example Usage Script
42
+
43
+
44
+ ### Read Block Number
45
+
46
+ To run the example:
47
+
48
+ ```bash
49
+ python examples/example-read-block-number.py
50
+ ```
51
+
52
+ See [example-read-block-number.py](https://github.com/0xOstium/ostium_python_sdk/blob/main/examples/example-read-block-number.py) for an example of how to use the SDK.
53
+
54
+ ### Read Positions
55
+
56
+ To run the example:
57
+
58
+ ```bash
59
+ python examples/example-read-positions.py
60
+ ```
61
+
62
+ See [example-read-positions.py](https://github.com/0xOstium/ostium_python_sdk/blob/main/examples/example-read-positions.py) for an example of how to use the SDK.
63
+
64
+
65
+ ### Get Feed Prices
66
+
67
+ To open a trade you need the latest feed price.
68
+
69
+ See this example script on how to get the latest feed prices.
70
+
71
+ ```bash
72
+ python examples/example-get-prices.py
73
+ ```
74
+
75
+ See [example-get-prices.py](https://github.com/0xOstium/ostium_python_sdk/blob/main/examples/example-get-prices.py) for an example of how to use the SDK.
76
+
77
+
78
+
79
+ ### Get Balance of an Address
80
+
81
+
82
+
83
+ See this example script on how to get the latest feed prices.
84
+
85
+ ```bash
86
+ python examples/example-get-balance.py
87
+ ```
88
+
89
+ See [example-get-balance.py](https://github.com/0xOstium/ostium_python_sdk/blob/main/examples/example-get-balance.py) for an example of how to use the SDK.
90
+
91
+
92
+
93
+