Skip to content

Commit

Permalink
feat: add query markets (#250)
Browse files Browse the repository at this point in the history
* feat: add query markets

* feat: add query markets

* feat: add query markets
  • Loading branch information
matthiasmatt authored Jul 21, 2023
1 parent 6784775 commit c87d0f6
Show file tree
Hide file tree
Showing 11 changed files with 304 additions and 63 deletions.
13 changes: 7 additions & 6 deletions pysdk/.pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: end-of-file-fixer
exclude: nibiru/proto/.+
- id: end-of-file-fixer
exclude: nibiru/proto/.+

- id: trailing-whitespace
exclude: nibiru/proto/.+
- id: trailing-whitespace
exclude: nibiru/proto/.+

- repo: https://github.com/psf/black
rev: 22.12.0
rev: 23.7.0
hooks:
- id: black
files: \.py$
Expand All @@ -26,9 +26,10 @@ repos:
- id: isort
files: \.py$
exclude: nibiru/proto/.+
args: [ "--profile", "black" ]
args: ["--profile", "black"]

- repo: https://github.com/pycqa/flake8
rev: 6.0.0
hooks:
- id: flake8
args: ["--max-line-length", "120"]
39 changes: 36 additions & 3 deletions pysdk/pysdk/query_clients/perp.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@
from nibiru_proto.nibiru.perp.v2 import query_pb2_grpc as perp_query

from pysdk.query_clients.util import QueryClient, deserialize
from pysdk.utils import from_sdk_dec
from pysdk.utils import from_sdk_dec, update_nested_fields


class PerpQueryClient(QueryClient):
"""
Perp allows to query the endpoints made available by the Nibiru Chain's PERP module.
Perp allows to query the endpoints made available by the Nibiru Chain's
PERP module.
"""

def __init__(self, channel: Channel):
Expand Down Expand Up @@ -57,9 +58,41 @@ def params(self):

return output

def markets(self):
"""
Get the all markets infromation.
"""
proto_output: perp_type.QueryMarketsResponse = self.query(
api_callable=self.api.QueryMarkets,
req=perp_type.QueryMarketsRequest(),
should_deserialize=False,
)

output = MessageToDict(proto_output)

fields = [
"ammMarkets.amm.baseReserve",
"ammMarkets.amm.quoteReserve",
"ammMarkets.amm.sqrtDepth",
"ammMarkets.amm.priceMultiplier",
"ammMarkets.amm.totalLong",
"ammMarkets.amm.totalShort",
"ammMarkets.market.maintenanceMarginRatio",
"ammMarkets.market.maxLeverage",
"ammMarkets.market.latestCumulativePremiumFraction",
"ammMarkets.market.exchangeFeeRatio",
"ammMarkets.market.ecosystemFundFeeRatio",
"ammMarkets.market.liquidationFeeRatio",
"ammMarkets.market.partialLiquidationRatio",
]

output = update_nested_fields(output, fields, from_sdk_dec)
return output

def position(self, pair: str, trader: str) -> dict:
"""
Get the trader position. Returns information about position notional, margin ratio
Get the trader position. Returns information about position notional,
margin ratio
unrealized pnl, size of the position etc.
Args:
Expand Down
114 changes: 92 additions & 22 deletions pysdk/pysdk/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

# reimplementation of cosmos-sdk/types/decimal.go
def to_sdk_dec(dec: float) -> str:
'''
"""
create a decimal from an input decimal.
valid must come in the form:
(-) whole integers (.) decimal integers
Expand All @@ -26,52 +26,52 @@ def to_sdk_dec(dec: float) -> str:
are provided in the string than the constant Precision.
CONTRACT - This function does not mutate the input str.
'''
"""
dec_str = str(dec)

if len(dec_str) == 0:
raise TypeError(f'Expected decimal string but got: {dec_str}')
raise TypeError(f"Expected decimal string but got: {dec_str}")

# first extract any negative symbol
neg = False
if dec_str[0] == '-':
if dec_str[0] == "-":
neg = True
dec_str = dec_str[1:]

if len(dec_str) == 0:
raise TypeError(f'Expected decimal string but got: {dec_str}')
raise TypeError(f"Expected decimal string but got: {dec_str}")

strs = dec_str.split('.')
strs = dec_str.split(".")
len_decs = 0
combined_str = strs[0]

if len(strs) == 2: # has a decimal place
len_decs = len(strs[1])
if len_decs == 0 or len(combined_str) == 0:
raise TypeError(f'Expected decimal string but got: {dec_str}')
raise TypeError(f"Expected decimal string but got: {dec_str}")
combined_str += strs[1]
elif len(strs) > 2:
raise TypeError(f'Expected decimal string but got: {dec_str}')
raise TypeError(f"Expected decimal string but got: {dec_str}")

if len_decs > PRECISION:
raise TypeError(
f'value \'{dec_str}\' exceeds max precision by {PRECISION-len_decs} decimal places: max precision {PRECISION}'
f"value '{dec_str}' exceeds max precision by {PRECISION-len_decs} decimal places: max precision {PRECISION}"
)

# add some extra zero's to correct to the Precision factor
zeros_to_add = PRECISION - len_decs
zeros = '0' * zeros_to_add
zeros = "0" * zeros_to_add
combined_str += zeros

try:
int(combined_str, 10)
except ValueError as err:
raise ValueError(
f'failed to set decimal string with base 10: {combined_str}'
f"failed to set decimal string with base 10: {combined_str}"
) from err

if neg:
return '-' + combined_str
return "-" + combined_str

return combined_str

Expand Down Expand Up @@ -120,31 +120,31 @@ def format_fields_nested(


def from_sdk_dec(dec_str: str) -> float:
if dec_str is None or dec_str == '':
if dec_str is None or dec_str == "":
return 0

if '.' in dec_str:
raise TypeError(f'expected a decimal string but got {dec_str} containing \'.\'')
if "." in dec_str:
raise TypeError(f"expected a decimal string but got {dec_str} containing '.'")

try:
int(dec_str)
except ValueError as err:
raise ValueError(f'failed to convert {dec_str} to a number') from err
raise ValueError(f"failed to convert {dec_str} to a number") from err

neg = False
if dec_str[0] == '-':
if dec_str[0] == "-":
neg = True
dec_str = dec_str[1:]

input_size = len(dec_str)
bz_str = ''
bz_str = ""
# case 1, purely decimal
if input_size <= PRECISION:
# 0. prefix
bz_str = '0.'
bz_str = "0."

# set relevant digits to 0
bz_str += '0' * (PRECISION - input_size)
bz_str += "0" * (PRECISION - input_size)

# set final digits
bz_str += dec_str
Expand All @@ -153,11 +153,11 @@ def from_sdk_dec(dec_str: str) -> float:
dec_point_place = input_size - PRECISION

bz_str = dec_str[:dec_point_place] # pre-decimal digits
bz_str += '.' # decimal point
bz_str += "." # decimal point
bz_str += dec_str[dec_point_place:] # pre-decimal digits

if neg:
bz_str = '-' + bz_str
bz_str = "-" + bz_str

return float(bz_str)

Expand Down Expand Up @@ -386,3 +386,73 @@ def _count_diff_hashable(actual, expected):
diff = _Mismatch(0, cnt_t, elem)
result.append(diff)
return result


def update_nested_fields(
d: dict, fields: List[str], func: Callable[[Any], Any]
) -> dict:
"""
Update specified fields in a nested dictionary.
This function recursively traverses a dictionary and updates specified fields
using a provided function. Field names should be specified as a list of strings,
where each string represents a nested path to the key that should be updated
(e.g., "key1.key2.key3").
Args:
d: The dictionary to update.
fields: A list of fields to update, specified as strings representing key paths.
func: A function to apply to each specified field.
Returns:
The updated dictionary.
"""
fields_set = {tuple(field.split(".")) for field in fields}

def helper(d: Union[dict, list], keys: tuple):
if isinstance(d, dict):
for key, value in d.items():
new_keys = keys + (key,)
if new_keys in fields_set:
d[key] = func(value)
elif isinstance(value, (dict, list)):
helper(value, new_keys)
elif isinstance(d, list):
for item in d:
helper(item, keys)

helper(d, ())
return d


def assert_subset(result, expected, path=None):
"""
Check if all values in expected are equal to the values in result.
This function iteratively checks if the expected values match the values in result. It traverses nested dictionaries
and lists, checking for matching values while ignoring missing keys in the expected dictionary.
Args:
result: The result dictionary.
expected: The expected dictionary.
path: The path to the current key (used for error messages).
Raises:
AssertionError: If a value in expected does not match the corresponding value in result.
"""
if path is None:
path = []

for key, value in expected.items():
if isinstance(value, dict):
assert key in result, f"Key {key} not found in result at path {path}"
assert_subset(result[key], value, path + [key])
elif isinstance(value, list):
assert key in result, f"Key {key} not found in result at path {path}"
for i, item in enumerate(value):
assert_subset(result[key][i], item, path + [key, i])
else:
assert key in result, f"Key {key} not found in result at path {path}"
assert (
result[key] == value
), f"Value {result[key]} at path {path + [key]} does not match expected value {value}"
Empty file modified pysdk/scripts/docgen.sh
100644 → 100755
Empty file.
Empty file modified pysdk/scripts/fmt.sh
100644 → 100755
Empty file.
Empty file modified pysdk/scripts/get_nibid.sh
100644 → 100755
Empty file.
Empty file modified pysdk/scripts/get_pricefeeder.sh
100644 → 100755
Empty file.
6 changes: 3 additions & 3 deletions pysdk/scripts/localnet.sh
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ echo_success() {

# Flag parsing: --flag-name (BASH_VAR_NAME)
#
# --no-build ($FLAG_NO_BUILD): toggles whether to build from source. The default
# behavior of the script is to run make install.
FLAG_NO_BUILD=false
# --no-build ($FLAG_NO_BUILD): toggles whether to build from source. The default
# behavior of the script is to run make install.
FLAG_NO_BUILD=false

build_from_source() {
echo_info "Building from source..."
Expand Down
Empty file modified pysdk/scripts/run_pricefeed.sh
100644 → 100755
Empty file.
69 changes: 65 additions & 4 deletions pysdk/tests/perp_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import tests
from pysdk import Msg
from pysdk import pytypes as pt
from pysdk.utils import assert_subset

PRECISION = 6

Expand Down Expand Up @@ -81,6 +82,66 @@ def test_perp_query_position(sdk_val: pysdk.Sdk):
tests.raises(ok_errors, err)


def test_perp_query_market(sdk_val: pysdk.Sdk):
output = sdk_val.query.perp.markets()

expected_to_be_equal = {
"ammMarkets": [
{
"market": {
"pair": "ubtc:unusd",
"enabled": True,
"maintenanceMarginRatio": 0.0625,
"maxLeverage": 10.0,
"latestCumulativePremiumFraction": 0.0,
"exchangeFeeRatio": 0.001,
"ecosystemFundFeeRatio": 0.001,
"liquidationFeeRatio": 0.05,
"partialLiquidationRatio": 0.5,
"fundingRateEpochId": "30 min",
"twapLookbackWindow": "1800s",
"prepaidBadDebt": {"denom": "unusd", "amount": "0"},
},
"amm": {
"pair": "ubtc:unusd",
"baseReserve": 30000000000000.0,
"quoteReserve": 30000000000000.0,
"sqrtDepth": 30000000000000.0,
"totalLong": 0.0,
"totalShort": 0.0,
},
},
{
"market": {
"pair": "ueth:unusd",
"enabled": True,
"maintenanceMarginRatio": 0.0625,
"maxLeverage": 10.0,
"latestCumulativePremiumFraction": 0.0,
"exchangeFeeRatio": 0.001,
"ecosystemFundFeeRatio": 0.001,
"liquidationFeeRatio": 0.05,
"partialLiquidationRatio": 0.5,
"fundingRateEpochId": "30 min",
"twapLookbackWindow": "1800s",
"prepaidBadDebt": {"denom": "unusd", "amount": "0"},
},
"amm": {
"pair": "ueth:unusd",
"baseReserve": 30000000000000.0,
"quoteReserve": 30000000000000.0,
"sqrtDepth": 30000000000000.0,
"totalLong": 0.0,
"totalShort": 0.0,
},
},
]
}

# have to assert subset since i don't want to check the price which can change in loclanet
assert_subset(output, expected_to_be_equal)


@pytest.mark.order(after="test_perp_query_position")
def test_perp_query_all_positions(sdk_val: pysdk.Sdk):
positions_map: Dict[str, dict] = sdk_val.query.perp.all_positions(
Expand All @@ -96,10 +157,10 @@ def test_perp_query_all_positions(sdk_val: pysdk.Sdk):
tests.dict_keys_must_match(
position_resp,
[
'position',
'position_notional',
'unrealized_pnl',
'margin_ratio',
"position",
"position_notional",
"unrealized_pnl",
"margin_ratio",
],
)

Expand Down
Loading

0 comments on commit c87d0f6

Please sign in to comment.