Repository: ccxt/ccxt
Pull request: fix(bitstamp): ws orders parsing
fix #26298

Problem statement:
Bitstamp: order updates are not handled and normalised correctly
### Operating System

_No response_

### Programming Languages

_No response_

### CCXT Version

4.4.91

### Description

I created a limit order and listening on watch_orders() separately. However, none of the fields `filled, remaining, status` ever being populated irrespective if order is opened, filled or cancelled. I have attached the 'order' response received when order was indeed opened.

### Code

```
orders = await exchange.watch_orders(symbol)
print(orders)

gives - 

  [{'amount': 15.0,
  'average': None,
  'clientOrderId': None,
  'cost': None,
  'datetime': '2025-06-25T09:24:34.000Z',
  'fee': None,
  'fees': [],
  'filled': None,
  'id': '1892386814689280',
  'info': {'amount': 15,
           'amount_at_create': '15.00000',
           'amount_str': '15.00000',
           'amount_traded': '0',
           'datetime': '1750843474',
           'id': 1892386814689280,
           'id_str': '1892386814689280',
           'is_liquidation': False,
           'microtimestamp': '1750843474337000',
           'order_subtype': 0,
           'order_type': 1,
           'price': 0.9,
           'price_str': '0.90000',
           'trade_account_id': 45645645},
  'lastTradeTimestamp': None,
  'lastUpdateTimestamp': None,
  'postOnly': None,
  'price': 0.9,
  'reduceOnly': None,
  'remaining': None,
  'side': 'sell',
  'status': None,
  'stopLossPrice': None,
  'stopPrice': None,
  'symbol': 'USDC/EUR',
  'takeProfitPrice': None,
  'timeInForce': None,
  'timestamp': 1750843474000,
  'trades': [],
  'triggerPrice': None,
  'type': None}]

```

Hints from issue discussion:

Commit message: fix(bitstamp): ws orders parsing

Patch to review:
diff --git a/ts/src/pro/bitstamp.ts b/ts/src/pro/bitstamp.ts
index 9439a00bb4ce6..bd861267d226e 100644
--- a/ts/src/pro/bitstamp.ts
+++ b/ts/src/pro/bitstamp.ts
@@ -6,6 +6,7 @@ import { ArgumentsRequired, AuthenticationError } from '../base/errors.js';
 import { ArrayCache, ArrayCacheBySymbolById } from '../base/ws/Cache.js';
 import type { Int, Str, OrderBook, Order, Trade, Dict } from '../base/types.js';
 import Client from '../base/ws/Client.js';
+import { Precise } from '../base/Precise.js';
 
 //  ---------------------------------------------------------------------------
 
@@ -318,6 +319,7 @@ export default class bitstamp extends bitstampRest {
         //        "price_str":"1000.00"
         //     },
         //     "channel":"private-my_orders_ltcusd-4848701",
+        //     "event": "order_deleted" // field only present for cancelOrder
         // }
         //
         const channel = this.safeString (message, 'channel');
@@ -330,6 +332,7 @@ export default class bitstamp extends bitstampRest {
         const subscription = this.safeValue (client.subscriptions, channel);
         const symbol = this.safeString (subscription, 'symbol');
         const market = this.market (symbol);
+        order['event'] = this.safeString (message, 'event');
         const parsed = this.parseWsOrder (order, market);
         stored.append (parsed);
         client.resolve (this.orders, channel);
@@ -337,23 +340,53 @@ export default class bitstamp extends bitstampRest {
 
     parseWsOrder (order, market = undefined) {
         //
-        //   {
-        //        "id":"1463471322288128",
-        //        "id_str":"1463471322288128",
-        //        "order_type":1,
-        //        "datetime":"1646127778",
-        //        "microtimestamp":"1646127777950000",
-        //        "amount":0.05,
-        //        "amount_str":"0.05000000",
-        //        "price":1000,
-        //        "price_str":"1000.00"
+        //    {
+        //        "id": "1894876776091648",
+        //        "id_str": "1894876776091648",
+        //        "order_type": 0,
+        //        "order_subtype": 0,
+        //        "datetime": "1751451375",
+        //        "microtimestamp": "1751451375070000",
+        //        "amount": 1.1,
+        //        "amount_str": "1.10000000",
+        //        "amount_traded": "0",
+        //        "amount_at_create": "1.10000000",
+        //        "price": 10.23,
+        //        "price_str": "10.23",
+        //        "is_liquidation": false,
+        //        "trade_account_id": 0
         //    }
         //
         const id = this.safeString (order, 'id_str');
-        const orderType = this.safeStringLower (order, 'order_type');
+        const orderTypeRaw = this.safeStringLower (order, 'order_type');
+        const side = (orderTypeRaw === '1') ? 'sell' : 'buy';
+        const orderSubTypeRaw = this.safeStringLower (order, 'order_subtype'); // https://www.bitstamp.net/websocket/v2/#:~:text=order_subtype
+        let orderType: Str = undefined;
+        let timeInForce: Str = undefined;
+        if (orderSubTypeRaw === '0') {
+            orderType = 'limit';
+        } else if (orderSubTypeRaw === '2') {
+            orderType = 'market';
+        } else if (orderSubTypeRaw === '4') {
+            orderType = 'limit';
+            timeInForce = 'IOC';
+        } else if (orderSubTypeRaw === '6') {
+            orderType = 'limit';
+            timeInForce = 'FOK';
+        } else if (orderSubTypeRaw === '8') {
+            orderType = 'limit';
+            timeInForce = 'GTD';
+        }
         const price = this.safeString (order, 'price_str');
         const amount = this.safeString (order, 'amount_str');
-        const side = (orderType === '1') ? 'sell' : 'buy';
+        const filled = this.safeString (order, 'amount_traded');
+        const event = this.safeString (order, 'event');
+        let status = undefined;
+        if (event === 'order_deleted') {
+            status = 'canceled';
+        } else if (Precise.stringEq (filled, amount)) {
+            status = 'closed';
+        }
         const timestamp = this.safeTimestamp (order, 'datetime');
         market = this.safeMarket (undefined, market);
         const symbol = market['symbol'];
@@ -365,8 +398,8 @@ export default class bitstamp extends bitstampRest {
             'timestamp': timestamp,
             'datetime': this.iso8601 (timestamp),
             'lastTradeTimestamp': undefined,
-            'type': undefined,
-            'timeInForce': undefined,
+            'type': orderType,
+            'timeInForce': timeInForce,
             'postOnly': undefined,
             'side': side,
             'price': price,
@@ -375,9 +408,9 @@ export default class bitstamp extends bitstampRest {
             'amount': amount,
             'cost': undefined,
             'average': undefined,
-            'filled': undefined,
+            'filled': filled,
             'remaining': undefined,
-            'status': undefined,
+            'status': status,
             'fee': undefined,
             'trades': undefined,
         }, market);
@@ -445,6 +478,7 @@ export default class bitstamp extends bitstampRest {
         //         "price_str":"1000.00"
         //         },
         //         "channel":"private-my_orders_ltcusd-4848701",
+        //         "event": "order_deleted" // field only present for cancelOrder
         //     }
         //
         const channel = this.safeString (message, 'channel');


Code review comments:
diff_hunk: @@ -330,30 +332,61 @@ export default class bitstamp extends bitstampRest {
         const subscription = this.safeValue (client.subscriptions, channel);
         const symbol = this.safeString (subscription, 'symbol');
         const market = this.market (symbol);
+        order['event'] = this.safeString (message, 'event');
         const parsed = this.parseWsOrder (order, market);
         stored.append (parsed);
         client.resolve (this.orders, channel);
     }
 
     parseWsOrder (order, market = undefined) {
         //
-        //   {
-        //        "id":"1463471322288128",
-        //        "id_str":"1463471322288128",
-        //        "order_type":1,
-        //        "datetime":"1646127778",
-        //        "microtimestamp":"1646127777950000",
-        //        "amount":0.05,
-        //        "amount_str":"0.05000000",
-        //        "price":1000,
-        //        "price_str":"1000.00"
+        //    {
+        //        "id": "1894876776091648",
+        //        "id_str": "1894876776091648",
+        //        "order_type": 0,
+        //        "order_subtype": 0,
+        //        "datetime": "1751451375",
+        //        "microtimestamp": "1751451375070000",
+        //        "amount": 1.1,
+        //        "amount_str": "1.10000000",
+        //        "amount_traded": "0",
+        //        "amount_at_create": "1.10000000",
+        //        "price": 10.23,
+        //        "price_str": "10.23",
+        //        "is_liquidation": false,
+        //        "trade_account_id": 0
         //    }
         //
         const id = this.safeString (order, 'id_str');
-        const orderType = this.safeStringLower (order, 'order_type');
+        const orderTypeRaw = this.safeStringLower (order, 'order_type');
+        const side = (orderTypeRaw === '1') ? 'sell' : 'buy';
+        const orderSubTypeRaw = this.safeStringLower (order, 'order_subtype'); // https://www.bitstamp.net/websocket/v2/#:~:text=order_subtype
+        let orderType: Str = undefined;
+        let timeInForce: Str = undefined;
+        if (orderSubTypeRaw === '0') {
+            orderType = 'limit';
+        } else if (orderSubTypeRaw === '2') {
+            orderType = 'market';
+        } else if (orderSubTypeRaw === '4') {
+            orderType = 'limit';
+            timeInForce = 'IOC';
+        } else if (orderSubTypeRaw === '6') {
+            orderType = 'limit';
+            timeInForce = 'FOK';
+        } else if (orderSubTypeRaw === '8') {
+            orderType = 'limit';
+            timeInForce = 'GTD';
+        }
         const price = this.safeString (order, 'price_str');
         const amount = this.safeString (order, 'amount_str');
-        const side = (orderType === '1') ? 'sell' : 'buy';
+        const filled = this.safeString (order, 'amount_traded');
+        const event = this.safeString (order, 'event');
+        let status = undefined;
+        if (event === 'order_deleted') {
+            status = 'canceled';
+        } else if (Precise.stringEq (filled, amount)) {
path: ts/src/pro/bitstamp.ts
text: @user1:
@author can't the filled be slightly smaller because of fees and stuff like that? can't we infer this using a different field?

@author:
i tested and got same numbers in amount & filled, commissions does not matter here, seems exchange might be taking the commission from USDT (quote). i was unable to infer 'closed' otherwise. we can remove that if you think so

@author:
anyway let me recheck

@user1:
@author  
![image](https://github.com/user-attachments/assets/408d693b-9c32-4293-9cc0-e3ac1f79212b)


We're not supporting it yet (createMarketBuyWithCost) but the exchange supports it, in that case you provide the `cost` and not the amount, so not sure if amount/filled are going to be the same in those cases

btw can you add support for it since you're already touching this exchange?

https://docs.bitvavo.com/docs/rest-api/create-order/

@author:
I restested with many different variations and there is not clear indication of the status, and we can only derive status this way. we can remove that, but maybe it would help users to have this. idk as you decide

@author:

> @author ![image](https://private-user-images.githubusercontent.com/43336371/461617423-408d693b-9c32-4293-9cc0-e3ac1f79212b.png?
> [docs.bitvavo.com/docs/rest-api/create-order](https://docs.bitvavo.com/docs/rest-api/create-order/)

maybe you mistakenly took that exchange? we are now editing bitstamp, and your referred one is bitvavo. can you recheck?


@user1:
@author arghhh sorry