Skip to content

ProtoOAPositionUnrealizedPnL.netUnrealizedPnL is compiled as int32 (the proto says int64), so values get truncated #50

Open

Description

@Forgetmypassword

In the official proto files (spotware/openapi-proto-messages, since cServer 86), netUnrealizedPnL is int64:

message ProtoOAPositionUnrealizedPnL {
    required int64 positionId = 1;
    required int64 grossUnrealizedPnL = 2;
    required int64 netUnrealizedPnL = 3;
}

The compiled ctrader_open_api/messages/OpenApiModelMessages_pb2.py shipped in ctrader-open-api==0.9.2 (and in the current main) declares netUnrealizedPnL as int32, while grossUnrealizedPnL next to it is int64. Protobuf truncates a 64-bit varint that is read into an int32 field. So every value outside ±2^31 wraps modulo 2^32.

For accounts with moneyDigits = 8 (for example, USDT deposit assets) that limit is only ±21.47 in the deposit currency, so the field is wrong for most real positions. We see this in production. On the wire, the server sends the correct int64 value, which equals gross + commission + swap. Only the SDK's decoding corrupts it.

Reproduction (offline, no credentials needed):

# pip install ctrader-open-api==0.9.2
from ctrader_open_api.messages.OpenApiModelMessages_pb2 import ProtoOAPositionUnrealizedPnL


def varint(n: int) -> bytes:
    n &= (1 << 64) - 1  # int64 two's complement, as protobuf encodes it on the wire
    out = bytearray()
    while True:
        b = n & 0x7F
        n >>= 7
        out.append(b | (0x80 if n else 0))
        if not n:
            return bytes(out)


# What the server sends: position 1, gross 58.3651, net 61.0371 (moneyDigits 8)
wire = b"\x08" + varint(1) + b"\x10" + varint(5_836_510_000) + b"\x18" + varint(6_103_710_000)

msg = ProtoOAPositionUnrealizedPnL()
msg.ParseFromString(wire)
print("gross", msg.grossUnrealizedPnL)  # 5836510000 -> correct
print("net  ", msg.netUnrealizedPnL)    # 1808742704 -> truncated to 32 bits; expected 6103710000

We confirmed this against live ProtoOAGetPositionUnrealizedPnLRes messages. We decoded the raw payload by hand and with the SDK: the raw int64 value is always correct, and the SDK value is always the raw value truncated to 32 bits.

Other differences between the compiled messages and the proto (harmless as far as we can tell):

  • ProtoOADealListByPositionIdRes.hasMore: proto bool, SDK int64
  • ProtoOADynamicLeverageTier.leverage: proto int32, SDK int64

Suggested fix: regenerate the _pb2 modules from the current proto files and release a new version.

Workaround for users: do not read netUnrealizedPnL through the SDK. Either decode the payload bytes yourself, or compute the net from grossUnrealizedPnL + the position's commission + swap (all int64).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions