Skip to content

Fix float_to_wire returning "-0" - #313

Open
BhariGowda wants to merge 1 commit into
hyperliquid-dex:masterfrom
BhariGowda:fix/float-to-wire-negative-zero
Open

Fix float_to_wire returning "-0"#313
BhariGowda wants to merge 1 commit into
hyperliquid-dex:masterfrom
BhariGowda:fix/float-to-wire-negative-zero

Conversation

@BhariGowda

@BhariGowda BhariGowda commented Aug 22, 2026

Copy link
Copy Markdown

float_to_wire has a guard meant to turn negative zero into "0", but it compares the pre-normalization string. That string is always fixed to 8 decimal places, so it is "-0.00000000" and never equals "-0". The "-0" only appears after Decimal.normalize(), which runs afterwards.

On master:

>>> from hyperliquid.utils.signing import float_to_wire
>>> float_to_wire(-0.0)
'-0'
>>> float_to_wire(-1e-13)
'-0'

float_to_wire produces the p and s fields of every order wire and the triggerPx of every trigger order, so this goes out on the wire as an order price or size of "-0".

It is reachable from ordinary caller arithmetic. round(-1e-9, 2) evaluates to -0.0 in Python, so sizing code of the shape sz = round(target - current, 4) hits it whenever the difference is a small negative number.

The same function exists in hyperliquid-rust-sdk as float_to_string_for_hashing (src/helpers.rs). It is the same algorithm, format to 8 decimals then strip trailing zeros, except that it applies the "-0" check after the stripping rather than before:

pub(crate) fn float_to_string_for_hashing(x: f64) -> String {
    let mut x = format!("{:.*}", WIRE_DECIMALS.into(), x);
    while x.ends_with('0') {
        x.pop();
    }
    if x.ends_with('.') {
        x.pop();
    }
    if x == "-0" {
        "0".to_string()
    } else {
        x
    }
}

and it pins the result with assertions:

assert_eq!(float_to_string_for_hashing(0.), "0".to_string());
assert_eq!(float_to_string_for_hashing(-0.), "0".to_string());
assert_eq!(float_to_string_for_hashing(-0.0000), "0".to_string());

Running all 13 assertions from that test against float_to_wire, master passes 11 and fails exactly the two -0. cases. With this change it passes 13 of 13.

The Python guard has never fired. f"{x:.8f}" always emits 8 fractional digits, so it cannot produce the two character string "-0" for any input: -0.0, -1e-300 and -1e-13 all format to "-0.00000000", and nan, inf and -inf format to "nan", "inf" and "-inf". It has sat in this position since it was introduced in 36dac26 (0.1.21), so this is dead code rather than a regression.

The fix moves the check to after normalize(), where the value it is looking for actually exists. Added a test covering negative zero, plain zero, and a couple of ordinary signed values so the sign handling does not regress.

Ran pytest tests/signing_test.py (14 passed) plus black, isort and flake8 with the repo's configured settings.

The guard that was meant to turn negative zero into "0" compares the
pre-normalization string, which is always fixed to 8 decimal places
("-0.00000000"), so it never matched and was dead code. The "-0" only
appears after Decimal.normalize(), so the check has to run on the
normalized output.

Reproduces on master:

    >>> float_to_wire(-0.0)
    '-0'
    >>> float_to_wire(-1e-13)
    '-0'

Both are reachable from ordinary caller arithmetic, for example
round(-1e-9, 2) evaluates to -0.0, which then goes out on the wire as an
order size or price of "-0".

Moved the check after normalize() and added a test covering negative
zero, plain zero and a couple of ordinary values so the sign handling
does not regress.
@BhariGowda BhariGowda closed this Aug 22, 2026
@BhariGowda BhariGowda reopened this Aug 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant