ostium-python-sdk 0.1.42__tar.gz → 0.2.0__tar.gz
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.
- {ostium_python_sdk-0.1.42 → ostium_python_sdk-0.2.0}/PKG-INFO +33 -8
- {ostium_python_sdk-0.1.42 → ostium_python_sdk-0.2.0}/README.md +19 -7
- {ostium_python_sdk-0.1.42 → ostium_python_sdk-0.2.0}/ostium_python_sdk/balance.py +6 -2
- {ostium_python_sdk-0.1.42 → ostium_python_sdk-0.2.0}/ostium_python_sdk/faucet.py +9 -3
- {ostium_python_sdk-0.1.42 → ostium_python_sdk-0.2.0}/ostium_python_sdk/formulae.py +0 -3
- {ostium_python_sdk-0.1.42 → ostium_python_sdk-0.2.0}/ostium_python_sdk/formulae_wrapper.py +7 -6
- {ostium_python_sdk-0.1.42 → ostium_python_sdk-0.2.0}/ostium_python_sdk/ostium.py +47 -34
- ostium_python_sdk-0.2.0/ostium_python_sdk/price.py +47 -0
- {ostium_python_sdk-0.1.42 → ostium_python_sdk-0.2.0}/ostium_python_sdk/sdk.py +49 -6
- {ostium_python_sdk-0.1.42 → ostium_python_sdk-0.2.0}/ostium_python_sdk/subgraph.py +10 -6
- ostium_python_sdk-0.2.0/ostium_python_sdk/utils.py +188 -0
- {ostium_python_sdk-0.1.42 → ostium_python_sdk-0.2.0}/ostium_python_sdk.egg-info/PKG-INFO +33 -8
- {ostium_python_sdk-0.1.42 → ostium_python_sdk-0.2.0}/setup.py +1 -1
- ostium_python_sdk-0.1.42/ostium_python_sdk/price.py +0 -37
- ostium_python_sdk-0.1.42/ostium_python_sdk/utils.py +0 -604
- {ostium_python_sdk-0.1.42 → ostium_python_sdk-0.2.0}/MANIFEST.in +0 -0
- {ostium_python_sdk-0.1.42 → ostium_python_sdk-0.2.0}/ostium_python_sdk/__init__.py +0 -0
- {ostium_python_sdk-0.1.42 → ostium_python_sdk-0.2.0}/ostium_python_sdk/abi/__init__.py +0 -0
- {ostium_python_sdk-0.1.42 → ostium_python_sdk-0.2.0}/ostium_python_sdk/abi/abi.py +0 -0
- {ostium_python_sdk-0.1.42 → ostium_python_sdk-0.2.0}/ostium_python_sdk/abi/faucet_abi.py +0 -0
- {ostium_python_sdk-0.1.42 → ostium_python_sdk-0.2.0}/ostium_python_sdk/config.py +0 -0
- {ostium_python_sdk-0.1.42 → ostium_python_sdk-0.2.0}/ostium_python_sdk/constants.py +0 -0
- {ostium_python_sdk-0.1.42 → ostium_python_sdk-0.2.0}/ostium_python_sdk/exceptions.py +0 -0
- {ostium_python_sdk-0.1.42 → ostium_python_sdk-0.2.0}/ostium_python_sdk.egg-info/SOURCES.txt +0 -0
- {ostium_python_sdk-0.1.42 → ostium_python_sdk-0.2.0}/ostium_python_sdk.egg-info/dependency_links.txt +0 -0
- {ostium_python_sdk-0.1.42 → ostium_python_sdk-0.2.0}/ostium_python_sdk.egg-info/requires.txt +0 -0
- {ostium_python_sdk-0.1.42 → ostium_python_sdk-0.2.0}/ostium_python_sdk.egg-info/top_level.txt +0 -0
- {ostium_python_sdk-0.1.42 → ostium_python_sdk-0.2.0}/pyproject.toml +0 -0
- {ostium_python_sdk-0.1.42 → ostium_python_sdk-0.2.0}/requirements-dev.txt +0 -0
- {ostium_python_sdk-0.1.42 → ostium_python_sdk-0.2.0}/requirements.txt +0 -0
- {ostium_python_sdk-0.1.42 → ostium_python_sdk-0.2.0}/setup.cfg +0 -0
- {ostium_python_sdk-0.1.42 → ostium_python_sdk-0.2.0}/tests/__init__.py +0 -0
- {ostium_python_sdk-0.1.42 → ostium_python_sdk-0.2.0}/tests/test_slippage.py +0 -0
- {ostium_python_sdk-0.1.42 → ostium_python_sdk-0.2.0}/tests/test_trade_get_tp_price.py +0 -0
- {ostium_python_sdk-0.1.42 → ostium_python_sdk-0.2.0}/tests/test_trade_liquidation_price.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: ostium-python-sdk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: A python based SDK developed for interacting with Ostium, a leveraged trading application for trading currencies, commodities, indices, crypto and more.
|
|
5
5
|
Home-page: https://github.com/0xOstium/ostium-python-sdk
|
|
6
6
|
Author: ami@ostium.io
|
|
@@ -100,8 +100,6 @@ Developed using:
|
|
|
100
100
|
You can instantiate the SDK with the following parameters.
|
|
101
101
|
Ostium Platform is deployed on Arbitrum. You can use the testnet or mainnet config via the `NetworkConfig` class, see below for an example.
|
|
102
102
|
|
|
103
|
-
|
|
104
|
-
|
|
105
103
|
```python
|
|
106
104
|
from dotenv import load_dotenv
|
|
107
105
|
from ostium_python_sdk import OstiumSDK, NetworkConfig
|
|
@@ -118,14 +116,12 @@ rpc_url = os.getenv('RPC_URL')
|
|
|
118
116
|
if not rpc_url:
|
|
119
117
|
raise ValueError("RPC_URL not found in .env file")
|
|
120
118
|
|
|
121
|
-
# Initialize SDK
|
|
119
|
+
# Initialize SDK (default: verbose=False for quiet operation)
|
|
122
120
|
configTestnet = NetworkConfig.testnet()
|
|
123
121
|
sdk = OstiumSDK(configTestnet, private_key)
|
|
124
122
|
|
|
125
|
-
#
|
|
126
|
-
|
|
127
|
-
# sdk = OstiumSDK(configMainnet, private_key)
|
|
128
|
-
#
|
|
123
|
+
# For verbose mode with detailed logging:
|
|
124
|
+
sdk = OstiumSDK(configTestnet, private_key, verbose=True)
|
|
129
125
|
```
|
|
130
126
|
|
|
131
127
|
<b>NOTE:</b> create a .env file with PRIVATE_KEY and RPC_URL to use the SDK. An RPC URL is required to use the SDK. You can get one by signing up for a free account at https://www.alchemy.com/ and creating an app.
|
|
@@ -275,6 +271,8 @@ trade_params = {
|
|
|
275
271
|
'asset_type': 0, # 0 for BTC, see pair_details above for other asset types
|
|
276
272
|
'direction': True, # True for Long, False for Short
|
|
277
273
|
'order_type': 'MARKET' # 'MARKET', 'LIMIT', or 'STOP'
|
|
274
|
+
#'tp': 0, # Take Profit price - if not specified or Zero means no TP
|
|
275
|
+
#'sl': 0, # Stop Loss price - if not specified or Zero means no SL
|
|
278
276
|
}
|
|
279
277
|
|
|
280
278
|
try:
|
|
@@ -329,6 +327,20 @@ except Exception as e:
|
|
|
329
327
|
|
|
330
328
|
```
|
|
331
329
|
|
|
330
|
+
|
|
331
|
+
**NOTE:** Use SDK method `get_open_trade_metrics` every so often while trade is open to get the trade's metrics such as:
|
|
332
|
+
|
|
333
|
+
- Funding fee
|
|
334
|
+
- Roll over fee
|
|
335
|
+
- Unrealized Pnl and Pnl Percent
|
|
336
|
+
- Total Profit
|
|
337
|
+
- Liquidation Price
|
|
338
|
+
|
|
339
|
+
```python
|
|
340
|
+
metrics = await sdk.get_open_trade_metrics(pair_id, trade_index)
|
|
341
|
+
print(metrics)
|
|
342
|
+
```
|
|
343
|
+
|
|
332
344
|
### Create a Short ETH Limit Order
|
|
333
345
|
|
|
334
346
|
This example shows how to create a short ETH limit order, 10% above the current ETHUSD price. So if price goes up 10% we order a Short ETH trade.
|
|
@@ -490,6 +502,19 @@ All notable changes to the Ostium Python SDK will be documented in this file.
|
|
|
490
502
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
491
503
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
492
504
|
|
|
505
|
+
## [0.2.0] - 2025-02-20
|
|
506
|
+
|
|
507
|
+
### Added
|
|
508
|
+
|
|
509
|
+
- Added sdk method `get_open_trade_metrics` to get open trade metrics such as:
|
|
510
|
+
- Funding fee
|
|
511
|
+
- Roll over fee
|
|
512
|
+
- Unrealized Pnl and Pnl Percent
|
|
513
|
+
- Total Profit
|
|
514
|
+
- Liquidation Price
|
|
515
|
+
|
|
516
|
+
- Added verbose mode to sdk (default is False, set to True to see debug logs)
|
|
517
|
+
|
|
493
518
|
## [0.1.36] - 2025-01-14
|
|
494
519
|
|
|
495
520
|
### Added
|
|
@@ -61,8 +61,6 @@ Developed using:
|
|
|
61
61
|
You can instantiate the SDK with the following parameters.
|
|
62
62
|
Ostium Platform is deployed on Arbitrum. You can use the testnet or mainnet config via the `NetworkConfig` class, see below for an example.
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
66
64
|
```python
|
|
67
65
|
from dotenv import load_dotenv
|
|
68
66
|
from ostium_python_sdk import OstiumSDK, NetworkConfig
|
|
@@ -79,14 +77,12 @@ rpc_url = os.getenv('RPC_URL')
|
|
|
79
77
|
if not rpc_url:
|
|
80
78
|
raise ValueError("RPC_URL not found in .env file")
|
|
81
79
|
|
|
82
|
-
# Initialize SDK
|
|
80
|
+
# Initialize SDK (default: verbose=False for quiet operation)
|
|
83
81
|
configTestnet = NetworkConfig.testnet()
|
|
84
82
|
sdk = OstiumSDK(configTestnet, private_key)
|
|
85
83
|
|
|
86
|
-
#
|
|
87
|
-
|
|
88
|
-
# sdk = OstiumSDK(configMainnet, private_key)
|
|
89
|
-
#
|
|
84
|
+
# For verbose mode with detailed logging:
|
|
85
|
+
sdk = OstiumSDK(configTestnet, private_key, verbose=True)
|
|
90
86
|
```
|
|
91
87
|
|
|
92
88
|
<b>NOTE:</b> create a .env file with PRIVATE_KEY and RPC_URL to use the SDK. An RPC URL is required to use the SDK. You can get one by signing up for a free account at https://www.alchemy.com/ and creating an app.
|
|
@@ -236,6 +232,8 @@ trade_params = {
|
|
|
236
232
|
'asset_type': 0, # 0 for BTC, see pair_details above for other asset types
|
|
237
233
|
'direction': True, # True for Long, False for Short
|
|
238
234
|
'order_type': 'MARKET' # 'MARKET', 'LIMIT', or 'STOP'
|
|
235
|
+
#'tp': 0, # Take Profit price - if not specified or Zero means no TP
|
|
236
|
+
#'sl': 0, # Stop Loss price - if not specified or Zero means no SL
|
|
239
237
|
}
|
|
240
238
|
|
|
241
239
|
try:
|
|
@@ -290,6 +288,20 @@ except Exception as e:
|
|
|
290
288
|
|
|
291
289
|
```
|
|
292
290
|
|
|
291
|
+
|
|
292
|
+
**NOTE:** Use SDK method `get_open_trade_metrics` every so often while trade is open to get the trade's metrics such as:
|
|
293
|
+
|
|
294
|
+
- Funding fee
|
|
295
|
+
- Roll over fee
|
|
296
|
+
- Unrealized Pnl and Pnl Percent
|
|
297
|
+
- Total Profit
|
|
298
|
+
- Liquidation Price
|
|
299
|
+
|
|
300
|
+
```python
|
|
301
|
+
metrics = await sdk.get_open_trade_metrics(pair_id, trade_index)
|
|
302
|
+
print(metrics)
|
|
303
|
+
```
|
|
304
|
+
|
|
293
305
|
### Create a Short ETH Limit Order
|
|
294
306
|
|
|
295
307
|
This example shows how to create a short ETH limit order, 10% above the current ETHUSD price. So if price goes up 10% we order a Short ETH trade.
|
|
@@ -8,14 +8,19 @@ REFRESH_BALANCE_SECONDS_INTERVAL = 60 * 5
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class Balance:
|
|
11
|
-
def __init__(self, w3: Web3, usdc_address: str) -> None:
|
|
11
|
+
def __init__(self, w3: Web3, usdc_address: str, verbose=False) -> None:
|
|
12
12
|
self.web3 = w3
|
|
13
13
|
self.usdc_address = usdc_address
|
|
14
|
+
self.verbose = verbose
|
|
14
15
|
self.usdc_contract = self.web3.eth.contract(
|
|
15
16
|
address=self.usdc_address, abi=usdc_abi)
|
|
16
17
|
# Format: {address: {'ether': value, 'usdc': value, 'last_refresh': timestamp}}
|
|
17
18
|
self.balances = {}
|
|
18
19
|
|
|
20
|
+
def log(self, message):
|
|
21
|
+
if self.verbose:
|
|
22
|
+
print(message)
|
|
23
|
+
|
|
19
24
|
def get_balance(self, address, refresh=False):
|
|
20
25
|
if address not in self.balances:
|
|
21
26
|
self.balances[address] = {'ether': None,
|
|
@@ -34,7 +39,6 @@ class Balance:
|
|
|
34
39
|
return self.balances[address]['ether'], self.balances[address]['usdc']
|
|
35
40
|
|
|
36
41
|
def read_balances(self, address):
|
|
37
|
-
# print(f'actual [{datetime.now().strftime("%Y-%m-%d %H:%M:%S")}] {address} - reading balances')
|
|
38
42
|
start_time = time.time()
|
|
39
43
|
self.balances[address] = {
|
|
40
44
|
'ether': Decimal(self.get_ether_balance(address)),
|
|
@@ -8,8 +8,9 @@ from ostium_python_sdk.abi.faucet_abi import faucet_abi # Import the ABI
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class Faucet:
|
|
11
|
-
def __init__(self, w3: Web3, private_key: str) -> None:
|
|
11
|
+
def __init__(self, w3: Web3, private_key: str, verbose=False) -> None:
|
|
12
12
|
self.web3 = w3
|
|
13
|
+
self.verbose = verbose
|
|
13
14
|
self.private_key = private_key
|
|
14
15
|
self.faucet_address = "0x6830C550814105d8B27bDAEC0DB391cAa7B967c8"
|
|
15
16
|
self.faucet_contract = self.web3.eth.contract(
|
|
@@ -17,6 +18,10 @@ class Faucet:
|
|
|
17
18
|
abi=faucet_abi
|
|
18
19
|
)
|
|
19
20
|
|
|
21
|
+
def log(self, message):
|
|
22
|
+
if self.verbose:
|
|
23
|
+
print(message)
|
|
24
|
+
|
|
20
25
|
def _format_waiting_time(self, next_request_time: int) -> str:
|
|
21
26
|
"""Format the waiting time into a human-readable string"""
|
|
22
27
|
current_time = int(time.time())
|
|
@@ -51,6 +56,7 @@ class Faucet:
|
|
|
51
56
|
|
|
52
57
|
def request_tokens(self) -> dict:
|
|
53
58
|
account = self._get_account()
|
|
59
|
+
self.log("Requesting tokens from faucet")
|
|
54
60
|
"""
|
|
55
61
|
Request testnet USDC tokens from the faucet.
|
|
56
62
|
Raises:
|
|
@@ -114,10 +120,10 @@ class Faucet:
|
|
|
114
120
|
def get_token_amount(self) -> int:
|
|
115
121
|
"""Get the amount of tokens that will be received from the faucet"""
|
|
116
122
|
try:
|
|
117
|
-
|
|
123
|
+
self.log("Calling tokenAmount() function") # Add debug print
|
|
118
124
|
return self.faucet_contract.functions.tokenAmount().call()
|
|
119
125
|
except Exception as e:
|
|
120
|
-
|
|
126
|
+
self.log(f"Error details: {str(e)}") # Add debug print
|
|
121
127
|
raise Exception(f"Failed to get token amount: {str(e)}")
|
|
122
128
|
|
|
123
129
|
def get_next_request_time(self, address: str) -> int:
|
|
@@ -96,11 +96,8 @@ def GetTradeLiquidationPrice(
|
|
|
96
96
|
leverage
|
|
97
97
|
)
|
|
98
98
|
|
|
99
|
-
# print('# liq_price_distance', liq_price_distance)
|
|
100
99
|
liq_price = open_price - liq_price_distance if long else open_price + liq_price_distance
|
|
101
|
-
|
|
102
100
|
liq_price = liq_price if liq_price > 0 else 0
|
|
103
|
-
# print('---> liq_price', liq_price)
|
|
104
101
|
return liq_price
|
|
105
102
|
|
|
106
103
|
except Exception as error:
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
from decimal import Decimal
|
|
2
|
-
from formulae import (PRECISION_18, PRECISION_2, PRECISION_6, GetCurrentRolloverFee,
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
from .formulae import (PRECISION_18, PRECISION_2, PRECISION_6, GetCurrentRolloverFee,
|
|
3
|
+
GetTradeFundingFee, GetTradeLiquidationPrice, GetTradeRolloverFee,
|
|
4
|
+
GetFundingRate, GetPriceImpact, CurrentTradeProfitRaw,
|
|
5
|
+
CurrentTotalProfitRaw, CurrentTotalProfitP)
|
|
6
6
|
from typing import Dict, Union
|
|
7
7
|
|
|
8
8
|
|
|
@@ -59,11 +59,11 @@ def get_funding_fee_long_short(pair_info, block_number):
|
|
|
59
59
|
return float(long_rate), float(short_rate)
|
|
60
60
|
|
|
61
61
|
|
|
62
|
-
def
|
|
62
|
+
def get_trade_metrics(trade_details, price_data, block_number):
|
|
63
63
|
"""
|
|
64
64
|
Calculate PNL and related metrics for a trade.
|
|
65
65
|
"""
|
|
66
|
-
if not trade_details or not price_data or not block_number
|
|
66
|
+
if not trade_details or not price_data or not block_number:
|
|
67
67
|
return {
|
|
68
68
|
'pnl': 0,
|
|
69
69
|
'pnl_raw': '0',
|
|
@@ -76,6 +76,7 @@ def get_trade_pnl(trade_details, pair_info, price_data, block_number):
|
|
|
76
76
|
'liquidation_price': 0
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
pair_info = trade_details['pair']
|
|
79
80
|
# Calculate current rollover fee
|
|
80
81
|
current_rollover_raw = GetCurrentRolloverFee(
|
|
81
82
|
pair_info['accRollover'],
|
|
@@ -2,6 +2,7 @@ import os
|
|
|
2
2
|
import traceback
|
|
3
3
|
from enum import Enum
|
|
4
4
|
from ostium_python_sdk.constants import PRECISION_2
|
|
5
|
+
from ostium_python_sdk.formulae_wrapper import get_trade_metrics
|
|
5
6
|
from web3 import Web3
|
|
6
7
|
from .abi.abi import usdc_abi, ostium_trading_abi, ostium_trading_storage_abi
|
|
7
8
|
from .utils import convert_to_scaled_integer, fromErrorCodeToMessage, get_tp_sl_prices, to_base_units
|
|
@@ -15,8 +16,9 @@ class OpenOrderType(Enum):
|
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
class Ostium:
|
|
18
|
-
def __init__(self, w3: Web3, usdc_address: str, ostium_trading_storage_address: str, ostium_trading_address: str, private_key: str) -> None:
|
|
19
|
+
def __init__(self, w3: Web3, usdc_address: str, ostium_trading_storage_address: str, ostium_trading_address: str, private_key: str, verbose=False) -> None:
|
|
19
20
|
self.web3 = w3
|
|
21
|
+
self.verbose = verbose
|
|
20
22
|
self.private_key = private_key
|
|
21
23
|
self.usdc_address = usdc_address
|
|
22
24
|
self.ostium_trading_storage_address = ostium_trading_storage_address
|
|
@@ -32,12 +34,20 @@ class Ostium:
|
|
|
32
34
|
|
|
33
35
|
self.slippage_percentage = 2 # 2%
|
|
34
36
|
|
|
37
|
+
def log(self, message):
|
|
38
|
+
if self.verbose:
|
|
39
|
+
print(message)
|
|
40
|
+
|
|
35
41
|
def set_slippage_percentage(self, slippage_percentage):
|
|
36
42
|
self.slippage_percentage = slippage_percentage
|
|
37
43
|
|
|
38
44
|
def get_slippage_percentage(self):
|
|
39
45
|
return self.slippage_percentage
|
|
40
46
|
|
|
47
|
+
def get_public_address(self):
|
|
48
|
+
public_address = self._get_account().address
|
|
49
|
+
return public_address
|
|
50
|
+
|
|
41
51
|
def _get_account(self) -> Account:
|
|
42
52
|
self._check_private_key()
|
|
43
53
|
"""Get account from stored private key"""
|
|
@@ -55,12 +65,13 @@ class Ostium:
|
|
|
55
65
|
"Private key is required for Ostium platform write-operations")
|
|
56
66
|
|
|
57
67
|
def perform_trade(self, trade_params, at_price):
|
|
68
|
+
self.log(f"Performing trade with params: {trade_params}")
|
|
58
69
|
account = self._get_account()
|
|
59
70
|
amount = to_base_units(trade_params['collateral'], decimals=6)
|
|
60
71
|
self.__approve(account, amount)
|
|
61
72
|
|
|
62
73
|
try:
|
|
63
|
-
|
|
74
|
+
self.log(f"Final trade parameters being sent: {trade_params}")
|
|
64
75
|
tp_price, sl_price = get_tp_sl_prices(trade_params)
|
|
65
76
|
|
|
66
77
|
trade = {
|
|
@@ -97,50 +108,52 @@ class Ostium:
|
|
|
97
108
|
signed_tx.raw_transaction)
|
|
98
109
|
trade_receipt = self.web3.eth.wait_for_transaction_receipt(
|
|
99
110
|
trade_tx_hash)
|
|
100
|
-
|
|
111
|
+
self.log(f"Trade Receipt: {trade_receipt}")
|
|
101
112
|
return trade_receipt
|
|
102
113
|
|
|
103
114
|
except Exception as e:
|
|
104
|
-
reason_string, suggestion = fromErrorCodeToMessage(
|
|
115
|
+
reason_string, suggestion = fromErrorCodeToMessage(
|
|
116
|
+
e, verbose=self.verbose)
|
|
105
117
|
print(
|
|
106
118
|
f"An error ({str(e)}) occurred during the trading process - parsed as {reason_string}")
|
|
107
119
|
raise Exception(
|
|
108
120
|
f'{reason_string}\n\n{suggestion}' if suggestion != None else reason_string)
|
|
109
121
|
|
|
110
|
-
def cancel_limit_order(self,
|
|
122
|
+
def cancel_limit_order(self, pair_id, trade_index):
|
|
111
123
|
account = self._get_account()
|
|
112
124
|
|
|
113
125
|
trade_tx = self.ostium_trading_contract.functions.cancelOpenLimitOrder(
|
|
114
|
-
int(
|
|
126
|
+
int(pair_id), int(trade_index)).build_transaction({'from': account.address})
|
|
115
127
|
trade_tx['nonce'] = self.get_nonce(account.address)
|
|
116
128
|
|
|
117
129
|
signed_tx = self.web3.eth.account.sign_transaction(
|
|
118
130
|
trade_tx, private_key=self.private_key)
|
|
119
131
|
trade_tx_hash = self.web3.eth.send_raw_transaction(
|
|
120
132
|
signed_tx.raw_transaction)
|
|
121
|
-
|
|
133
|
+
self.log(f"Cancel Limit Order TX Hash: {trade_tx_hash.hex()}")
|
|
122
134
|
|
|
123
135
|
trade_receipt = self.web3.eth.wait_for_transaction_receipt(
|
|
124
136
|
trade_tx_hash)
|
|
125
|
-
|
|
137
|
+
self.log(f"Cancel Limit Order Receipt: {trade_receipt}")
|
|
126
138
|
return trade_receipt
|
|
127
139
|
|
|
128
|
-
def close_trade(self,
|
|
140
|
+
def close_trade(self, pair_id, trade_index):
|
|
141
|
+
self.log(f"Closing trade for pair {pair_id}, index {trade_index}")
|
|
129
142
|
account = self._get_account()
|
|
130
143
|
|
|
131
144
|
trade_tx = self.ostium_trading_contract.functions.closeTradeMarket(
|
|
132
|
-
int(
|
|
145
|
+
int(pair_id), int(trade_index)).build_transaction({'from': account.address})
|
|
133
146
|
trade_tx['nonce'] = self.get_nonce(account.address)
|
|
134
147
|
|
|
135
148
|
signed_tx = self.web3.eth.account.sign_transaction(
|
|
136
149
|
trade_tx, private_key=self.private_key)
|
|
137
150
|
trade_tx_hash = self.web3.eth.send_raw_transaction(
|
|
138
151
|
signed_tx.raw_transaction)
|
|
139
|
-
|
|
152
|
+
self.log(f"Trade TX Hash: {trade_tx_hash.hex()}")
|
|
140
153
|
|
|
141
154
|
trade_receipt = self.web3.eth.wait_for_transaction_receipt(
|
|
142
155
|
trade_tx_hash)
|
|
143
|
-
|
|
156
|
+
self.log(f"Trade Receipt: {trade_receipt}")
|
|
144
157
|
return trade_receipt
|
|
145
158
|
|
|
146
159
|
def add_collateral(self, pairID, index, collateral):
|
|
@@ -157,11 +170,11 @@ class Ostium:
|
|
|
157
170
|
add_collateral_tx, private_key=self.private_key)
|
|
158
171
|
add_collateral_tx_hash = self.web3.eth.send_raw_transaction(
|
|
159
172
|
signed_tx.raw_transaction)
|
|
160
|
-
|
|
173
|
+
self.log(f"Add Collateral TX Hash: {add_collateral_tx_hash.hex()}")
|
|
161
174
|
|
|
162
175
|
add_collateral_receipt = self.web3.eth.wait_for_transaction_receipt(
|
|
163
176
|
add_collateral_tx_hash)
|
|
164
|
-
|
|
177
|
+
self.log(f"Add Collateral Receipt: {add_collateral_receipt}")
|
|
165
178
|
return add_collateral_receipt
|
|
166
179
|
|
|
167
180
|
except Exception as e:
|
|
@@ -169,20 +182,22 @@ class Ostium:
|
|
|
169
182
|
traceback.print_exc()
|
|
170
183
|
raise e
|
|
171
184
|
|
|
172
|
-
def update_tp(self,
|
|
185
|
+
def update_tp(self, pair_id, trade_index, tp_price):
|
|
186
|
+
self.log(
|
|
187
|
+
f"Updating TP for pair {pair_id}, index {trade_index} to {tp_price}")
|
|
173
188
|
account = self._get_account()
|
|
174
189
|
try:
|
|
175
|
-
tp_value = to_base_units(
|
|
190
|
+
tp_value = to_base_units(tp_price, decimals=18)
|
|
176
191
|
|
|
177
192
|
update_tp_tx = self.ostium_trading_contract.functions.updateTp(
|
|
178
|
-
int(
|
|
193
|
+
int(pair_id), int(trade_index), tp_value).build_transaction({'from': account.address})
|
|
179
194
|
update_tp_tx['nonce'] = self.get_nonce(account.address)
|
|
180
195
|
|
|
181
196
|
signed_tx = self.web3.eth.account.sign_transaction(
|
|
182
197
|
update_tp_tx, private_key=self.private_key)
|
|
183
198
|
update_tp_tx_hash = self.web3.eth.send_raw_transaction(
|
|
184
199
|
signed_tx.raw_transaction)
|
|
185
|
-
|
|
200
|
+
self.log(f"Update TP TX Hash: {update_tp_tx_hash.hex()}")
|
|
186
201
|
|
|
187
202
|
except Exception as e:
|
|
188
203
|
print("An error occurred during the update tp process:")
|
|
@@ -202,10 +217,11 @@ class Ostium:
|
|
|
202
217
|
update_sl_tx, private_key=self.private_key)
|
|
203
218
|
update_sl_tx_hash = self.web3.eth.send_raw_transaction(
|
|
204
219
|
signed_tx.raw_transaction)
|
|
205
|
-
|
|
220
|
+
self.log(f"Update SL TX Hash: {update_sl_tx_hash.hex()}")
|
|
206
221
|
|
|
207
222
|
except Exception as e:
|
|
208
|
-
reason_string, suggestion = fromErrorCodeToMessage(
|
|
223
|
+
reason_string, suggestion = fromErrorCodeToMessage(
|
|
224
|
+
str(e), verbose=self.verbose)
|
|
209
225
|
print(
|
|
210
226
|
f"An error occurred during the update sl process: {reason_string}")
|
|
211
227
|
raise Exception(
|
|
@@ -227,11 +243,11 @@ class Ostium:
|
|
|
227
243
|
approve_tx, private_key=self.private_key)
|
|
228
244
|
approve_tx_hash = self.web3.eth.send_raw_transaction(
|
|
229
245
|
signed_tx.raw_transaction)
|
|
230
|
-
|
|
246
|
+
self.log(f"Approval TX Hash: {approve_tx_hash.hex()}")
|
|
231
247
|
|
|
232
248
|
approve_receipt = self.web3.eth.wait_for_transaction_receipt(
|
|
233
249
|
approve_tx_hash)
|
|
234
|
-
|
|
250
|
+
self.log(f"Approval Receipt: {approve_receipt}")
|
|
235
251
|
|
|
236
252
|
def withdraw(self, amount, receiving_address):
|
|
237
253
|
account = self._get_account()
|
|
@@ -253,15 +269,16 @@ class Ostium:
|
|
|
253
269
|
transfer_tx, private_key=self.private_key)
|
|
254
270
|
transfer_tx_hash = self.web3.eth.send_raw_transaction(
|
|
255
271
|
signed_tx.raw_transaction)
|
|
256
|
-
|
|
272
|
+
self.log(f"Transfer TX Hash: {transfer_tx_hash.hex()}")
|
|
257
273
|
|
|
258
274
|
transfer_receipt = self.web3.eth.wait_for_transaction_receipt(
|
|
259
275
|
transfer_tx_hash)
|
|
260
|
-
|
|
276
|
+
self.log(f"Transfer Receipt: {transfer_receipt}")
|
|
261
277
|
return transfer_receipt
|
|
262
278
|
|
|
263
279
|
except Exception as e:
|
|
264
|
-
reason_string, suggestion = fromErrorCodeToMessage(
|
|
280
|
+
reason_string, suggestion = fromErrorCodeToMessage(
|
|
281
|
+
str(e), verbose=self.verbose)
|
|
265
282
|
print(
|
|
266
283
|
f"An error occurred during the transfer process: {reason_string}")
|
|
267
284
|
raise Exception(
|
|
@@ -269,10 +286,7 @@ class Ostium:
|
|
|
269
286
|
|
|
270
287
|
def update_limit_order(self, pair_id, index, pvt_key, price=None, tp=None, sl=None):
|
|
271
288
|
try:
|
|
272
|
-
print('update_limit_order called with pair_id', pair_id, 'index',
|
|
273
|
-
index, 'pvt_key', pvt_key, 'price', price, 'tp', tp, 'sl', sl)
|
|
274
289
|
account = self.web3.eth.account.from_key(pvt_key)
|
|
275
|
-
|
|
276
290
|
# Get existing order details (tbd why read from storage)
|
|
277
291
|
existing_order = self.ostium_trading_storage_contract.functions.getOpenLimitOrder(
|
|
278
292
|
account.address,
|
|
@@ -280,7 +294,7 @@ class Ostium:
|
|
|
280
294
|
int(index)
|
|
281
295
|
).call()
|
|
282
296
|
|
|
283
|
-
|
|
297
|
+
self.log(f"existing_order {existing_order}")
|
|
284
298
|
# Use existing values if new values are not provided
|
|
285
299
|
price_value = convert_to_scaled_integer(
|
|
286
300
|
price) if price is not None else existing_order[1] # openPrice
|
|
@@ -289,8 +303,6 @@ class Ostium:
|
|
|
289
303
|
sl_value = convert_to_scaled_integer(
|
|
290
304
|
sl) if sl is not None else existing_order[3] # sl
|
|
291
305
|
|
|
292
|
-
print('calling updateOpenLimitOrder with price_value',
|
|
293
|
-
price_value, 'tp_value', tp_value, 'sl_value', sl_value)
|
|
294
306
|
trade_tx = self.ostium_trading_contract.functions.updateOpenLimitOrder(
|
|
295
307
|
int(pair_id),
|
|
296
308
|
int(index),
|
|
@@ -305,15 +317,16 @@ class Ostium:
|
|
|
305
317
|
trade_tx, private_key=account.key)
|
|
306
318
|
trade_tx_hash = self.web3.eth.send_raw_transaction(
|
|
307
319
|
signed_tx.raw_transaction)
|
|
308
|
-
|
|
320
|
+
self.log(f"Update Limit Order TX Hash: {trade_tx_hash.hex()}")
|
|
309
321
|
|
|
310
322
|
trade_receipt = self.web3.eth.wait_for_transaction_receipt(
|
|
311
323
|
trade_tx_hash)
|
|
312
|
-
|
|
324
|
+
self.log(f"Update Limit Order Receipt: {trade_receipt}")
|
|
313
325
|
return trade_receipt
|
|
314
326
|
|
|
315
327
|
except Exception as e:
|
|
316
|
-
reason_string, suggestion = fromErrorCodeToMessage(
|
|
328
|
+
reason_string, suggestion = fromErrorCodeToMessage(
|
|
329
|
+
str(e), verbose=self.verbose)
|
|
317
330
|
print(
|
|
318
331
|
f"An error occurred during the update limit order process: {reason_string}")
|
|
319
332
|
raise Exception(
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import aiohttp
|
|
2
|
+
from typing import Tuple
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Price:
|
|
6
|
+
def __init__(self, verbose=False):
|
|
7
|
+
self.verbose = verbose
|
|
8
|
+
self.base_url = "https://listener.ostium.io"
|
|
9
|
+
|
|
10
|
+
def log(self, message):
|
|
11
|
+
if self.verbose:
|
|
12
|
+
print(message)
|
|
13
|
+
|
|
14
|
+
# Returns a list of price data, e.g: [{'feed_id': '0x00039d9e45394f473ab1f050a1b963e6b05351e52d71e507509ada0c95ed75b8', 'bid': 97241.43864211132, 'mid': 97243.36503172085, 'ask': 97245.2739217016, 'isMarketOpen': True, 'from': 'BTC', 'to': 'USD', 'timestampSeconds': 1740043714}, ...]
|
|
15
|
+
async def get_latest_prices(self):
|
|
16
|
+
"""
|
|
17
|
+
Fetches the latest prices from the Ostium price listener service.
|
|
18
|
+
Returns a dictionary of price data.
|
|
19
|
+
"""
|
|
20
|
+
async with aiohttp.ClientSession() as session:
|
|
21
|
+
async with session.get(f"{self.base_url}/PricePublish/latest-prices") as response:
|
|
22
|
+
if response.status == 200:
|
|
23
|
+
return await response.json()
|
|
24
|
+
else:
|
|
25
|
+
raise Exception(
|
|
26
|
+
f"Failed to fetch prices: {response.status}")
|
|
27
|
+
|
|
28
|
+
# Returns a json, e.g: {'feed_id': '0x00039d9e45394f473ab1f050a1b963e6b05351e52d71e507509ada0c95ed75b8', 'bid': 97241.43864211132, 'mid': 97243.36503172085, 'ask': 97245.2739217016, 'isMarketOpen': True, 'from': 'BTC', 'to': 'USD', 'timestampSeconds': 1740043714}
|
|
29
|
+
async def get_latest_price_json(self, from_asset: str, to_asset: str) -> Tuple[float, bool]:
|
|
30
|
+
prices = await self.get_latest_prices()
|
|
31
|
+
for price_data in prices:
|
|
32
|
+
if (price_data.get('from') == from_asset and
|
|
33
|
+
price_data.get('to') == to_asset):
|
|
34
|
+
self.log(f"get_latest_price_json: {price_data}")
|
|
35
|
+
return price_data
|
|
36
|
+
raise ValueError(f"No price found for pair: {from_asset}/{to_asset}")
|
|
37
|
+
|
|
38
|
+
# Returns a mid price and isMarketOpen tuple, e.g: (97243.36503172085, True)
|
|
39
|
+
async def get_price(self, from_currency, to_currency):
|
|
40
|
+
self.log(f"Getting price for {from_currency}/{to_currency}")
|
|
41
|
+
prices = await self.get_latest_prices()
|
|
42
|
+
for price_data in prices:
|
|
43
|
+
if (price_data.get('from') == from_currency and
|
|
44
|
+
price_data.get('to') == to_currency):
|
|
45
|
+
return float(price_data.get('mid', 0)), price_data.get('isMarketOpen', False)
|
|
46
|
+
raise ValueError(
|
|
47
|
+
f"No price found for pair: {from_currency}/{to_currency}")
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
from dotenv import load_dotenv
|
|
2
2
|
import os
|
|
3
3
|
from decimal import Decimal
|
|
4
|
+
|
|
5
|
+
from .formulae_wrapper import get_trade_metrics
|
|
4
6
|
from .constants import PRECISION_2, PRECISION_6, PRECISION_12, PRECISION_18, PRECISION_9
|
|
5
7
|
|
|
6
8
|
from ostium_python_sdk.faucet import Faucet
|
|
@@ -14,7 +16,8 @@ from .subgraph import SubgraphClient
|
|
|
14
16
|
|
|
15
17
|
|
|
16
18
|
class OstiumSDK:
|
|
17
|
-
def __init__(self, network: Union[str, NetworkConfig], private_key: str = None, rpc_url: str = None):
|
|
19
|
+
def __init__(self, network: Union[str, NetworkConfig], private_key: str = None, rpc_url: str = None, verbose=False):
|
|
20
|
+
self.verbose = verbose
|
|
18
21
|
load_dotenv()
|
|
19
22
|
self.private_key = private_key or os.getenv('PRIVATE_KEY')
|
|
20
23
|
# if not self.private_key:
|
|
@@ -52,20 +55,60 @@ class OstiumSDK:
|
|
|
52
55
|
self.network_config.contracts["usdc"],
|
|
53
56
|
self.network_config.contracts["tradingStorage"],
|
|
54
57
|
self.network_config.contracts["trading"],
|
|
55
|
-
private_key=self.private_key
|
|
58
|
+
private_key=self.private_key,
|
|
59
|
+
verbose=self.verbose
|
|
56
60
|
)
|
|
57
61
|
|
|
58
62
|
# Initialize subgraph client
|
|
59
|
-
self.subgraph = SubgraphClient(
|
|
63
|
+
self.subgraph = SubgraphClient(
|
|
64
|
+
url=self.network_config.graph_url, verbose=self.verbose)
|
|
60
65
|
|
|
61
|
-
self.balance = Balance(
|
|
62
|
-
|
|
66
|
+
self.balance = Balance(
|
|
67
|
+
self.w3, self.network_config.contracts["usdc"], verbose=self.verbose)
|
|
68
|
+
self.price = Price(verbose=self.verbose)
|
|
63
69
|
|
|
64
70
|
if self.network_config.is_testnet:
|
|
65
|
-
self.faucet = Faucet(self.w3, self.private_key
|
|
71
|
+
self.faucet = Faucet(self.w3, self.private_key,
|
|
72
|
+
verbose=self.verbose)
|
|
66
73
|
else:
|
|
67
74
|
self.faucet = None
|
|
68
75
|
|
|
76
|
+
def log(self, message):
|
|
77
|
+
if self.verbose:
|
|
78
|
+
print(message)
|
|
79
|
+
|
|
80
|
+
# if SDK instantiated with a private key, this function will return a given open trade metrics,
|
|
81
|
+
# such as: funding fee, roll over fee, Unrealized Pnl, Profit Percent, etc.
|
|
82
|
+
#
|
|
83
|
+
# Will thorw in case SDK instantiated with no private key
|
|
84
|
+
async def get_open_trade_metrics(self, pair_id, trade_index):
|
|
85
|
+
trader_public_address = self.ostium.get_public_address()
|
|
86
|
+
self.log(f"Trader public address: {trader_public_address}")
|
|
87
|
+
open_trades = await self.subgraph.get_open_trades(trader_public_address)
|
|
88
|
+
|
|
89
|
+
trade_details = None
|
|
90
|
+
|
|
91
|
+
if len(open_trades) == 0:
|
|
92
|
+
raise ValueError(f"No Open Trades for {trader_public_address}")
|
|
93
|
+
|
|
94
|
+
for t in open_trades:
|
|
95
|
+
if int(t['pair']['id']) == int(pair_id) and int(t['index']) == int(trade_index):
|
|
96
|
+
trade_details = t
|
|
97
|
+
break
|
|
98
|
+
|
|
99
|
+
if trade_details is None:
|
|
100
|
+
raise ValueError(
|
|
101
|
+
f"Trade not found for {trader_public_address} pair {pair_id} and index {trade_index}")
|
|
102
|
+
|
|
103
|
+
self.log(f"Trade details: {trade_details}")
|
|
104
|
+
# get the price for this trade's asset/feed
|
|
105
|
+
price_data = await self.price.get_latest_price_json(trade_details['pair']['from'], trade_details['pair']['to'])
|
|
106
|
+
self.log(f"Price data: {price_data} (need here bid, mid, ask prices)")
|
|
107
|
+
# get the block number
|
|
108
|
+
block_number = self.ostium.get_block_number()
|
|
109
|
+
self.log(f"Block number: {block_number}")
|
|
110
|
+
return get_trade_metrics(trade_details, price_data, block_number)
|
|
111
|
+
|
|
69
112
|
async def get_formatted_pairs_details(self) -> list:
|
|
70
113
|
"""
|
|
71
114
|
Get formatted details for all trading pairs, with proper decimal conversion.
|