3434GRIDPOOL_ID = int (GRIDPOOL_ID )
3535SERVER_URL = "grpc://electricity-trading-testing.api.frequenz.com:443?ssl=true"
3636
37+ MIN_QUANTITY_MW = Decimal ("0.1" )
38+ MIN_PRICE = Decimal (- 9999.0 )
39+ MAX_PRICE = Decimal (9999.0 )
3740
3841@pytest .fixture
3942async def set_up () -> dict [str , Any ]:
@@ -57,6 +60,7 @@ async def set_up() -> dict[str, Any]:
5760 price = Price (amount = Decimal ("56" ), currency = Currency .EUR )
5861 quantity = Power (mw = Decimal ("0.1" ))
5962 order_type = OrderType .LIMIT
63+ valid_until = None
6064
6165 return {
6266 "client" : client ,
@@ -65,26 +69,36 @@ async def set_up() -> dict[str, Any]:
6569 "price" : price ,
6670 "quantity" : quantity ,
6771 "order_type" : order_type ,
72+ "valid_until" : valid_until ,
6873 }
6974
7075
7176async def create_test_order (
7277 set_up : dict [str , Any ],
7378 side : MarketSide = MarketSide .BUY ,
7479 price : Price | None = None ,
80+ quantity : Power | None = None ,
7581 delivery_period : DeliveryPeriod | None = None ,
82+ delivery_area : DeliveryArea | None = None ,
83+ order_type : OrderType | None = None ,
84+ valid_until : datetime | None = None ,
7685) -> OrderDetail :
7786 """Create a test order with customizable parameters."""
7887 order_price = price or set_up ["price" ]
88+ order_quantity = quantity or set_up ["quantity" ]
7989 order_delivery_period = delivery_period or set_up ["delivery_period" ]
90+ order_delivery_area = delivery_area or set_up ["delivery_area" ]
91+ order_type = order_type or set_up ["order_type" ]
92+ order_valid_until = valid_until or set_up ["valid_until" ]
8093 order = await set_up ["client" ].create_gridpool_order (
8194 gridpool_id = GRIDPOOL_ID ,
82- delivery_area = set_up [ "delivery_area" ] ,
95+ delivery_area = order_delivery_area ,
8396 delivery_period = order_delivery_period ,
84- order_type = set_up [ " order_type" ] ,
97+ order_type = order_type ,
8598 side = side ,
8699 price = order_price ,
87- quantity = set_up ["quantity" ],
100+ quantity = order_quantity ,
101+ valid_until = order_valid_until ,
88102 tag = "api-integration-test" ,
89103 )
90104 return order # type: ignore
@@ -192,6 +206,17 @@ async def test_create_order_invalid_delivery_start_15_minutes_ago(
192206 with pytest .raises (ValueError , match = "delivery_period must be in the future" ):
193207 await create_test_order (set_up , delivery_period = delivery_period )
194208
209+ @pytest .mark .asyncio
210+ async def test_create_order_invalid_valid_until_one_hour_ago (
211+ set_up : dict [str , Any ]
212+ ) -> None :
213+ """Test creating an order with a passed valid until (one hour ago)."""
214+ valid_until = (datetime .now (timezone .utc ) - timedelta (hours = 1 )).replace (
215+ minute = 0 , second = 0 , microsecond = 0
216+ )
217+ with pytest .raises (ValueError , match = "valid_until must be in the future" ):
218+ await create_test_order (set_up , valid_until = valid_until )
219+
195220
196221@pytest .mark .asyncio
197222async def test_list_gridpool_orders (set_up : dict [str , Any ]) -> None :
@@ -376,6 +401,159 @@ async def test_stream_gridpool_trades(set_up: dict[str, Any]) -> None:
376401 except asyncio .TimeoutError :
377402 pytest .fail ("Streaming timed out, no trade received in 15 seconds" )
378403
404+ @pytest .mark .asyncio
405+ async def test_create_order_zero_quantity (set_up : dict [str , Any ]) -> None :
406+ """Test creating an order with zero quantity."""
407+ zero_quantity = Power (mw = Decimal ("0" ))
408+ with pytest .raises (ValueError , match = "Quantity must be strictly positive" ):
409+ await create_test_order (set_up , quantity = zero_quantity )
410+
411+
412+ @pytest .mark .asyncio
413+ async def test_create_order_negative_quantity (set_up : dict [str , Any ]) -> None :
414+ """Test creating an order with a negative quantity."""
415+ negative_quantity = Power (mw = Decimal ("-0.1" ))
416+ with pytest .raises (ValueError , match = "Quantity must be strictly positive" ):
417+ await create_test_order (set_up , quantity = negative_quantity )
418+
419+
420+ @pytest .mark .asyncio
421+ async def test_create_order_maximum_price_precision_exceeded (set_up : dict [str , Any ]) -> None :
422+ """Test creating an order with excessive decimal precision in price."""
423+ excessive_precision_price = Price (amount = Decimal ("56.123" ), currency = Currency .EUR )
424+ with pytest .raises (ValueError , match = "cannot have more than 2 decimal places" ):
425+ await create_test_order (set_up , price = excessive_precision_price )
426+
427+ @pytest .mark .asyncio
428+ async def test_create_order_maximum_quantity_precision_exceeded (set_up : dict [str , Any ]) -> None :
429+ """Test creating an order with excessive decimal precision in quantity."""
430+ excessive_precision_quantity = Power (mw = Decimal ("0.5001" ))
431+ with pytest .raises (ValueError , match = "The quantity cannot have more than 1 decimal." ):
432+ await create_test_order (set_up , quantity = excessive_precision_quantity )
433+
434+ @pytest .mark .asyncio
435+ async def test_cancel_non_existent_order (set_up : dict [str , Any ]) -> None :
436+ """Test canceling a non-existent order and expecting an error."""
437+ non_existent_order_id = 999999
438+ with pytest .raises (grpc .aio .AioRpcError ) as excinfo :
439+ await set_up ["client" ].cancel_gridpool_order (GRIDPOOL_ID , non_existent_order_id )
440+ assert excinfo .value .code () == grpc .StatusCode .UNAVAILABLE , "Cancelling non-existent order should return an error"
441+
442+
443+ @pytest .mark .asyncio
444+ async def test_cancel_already_cancelled_order (set_up : dict [str , Any ]) -> None :
445+ """Test cancelling an order twice to ensure idempotent behavior."""
446+ order = await create_test_order (set_up )
447+ await set_up ["client" ].cancel_gridpool_order (GRIDPOOL_ID , order .order_id )
448+ with pytest .raises (grpc .aio .AioRpcError ) as excinfo :
449+ cancelled_order = await set_up ["client" ].cancel_gridpool_order (GRIDPOOL_ID , order .order_id )
450+ assert excinfo .value .code () == grpc .StatusCode .INVALID_ARGUMENT , "Order is already cancelled"
451+
452+
453+ @pytest .mark .asyncio
454+ async def test_create_order_with_invalid_delivery_area (set_up : dict [str , Any ]) -> None :
455+ """Test creating an order with an invalid delivery area code."""
456+ invalid_delivery_area = DeliveryArea (code = "INVALID_CODE" , code_type = EnergyMarketCodeType .EUROPE_EIC )
457+ with pytest .raises (grpc .aio .AioRpcError ) as excinfo :
458+ await set_up ["client" ].create_gridpool_order (
459+ gridpool_id = GRIDPOOL_ID ,
460+ delivery_area = invalid_delivery_area ,
461+ delivery_period = set_up ["delivery_period" ],
462+ order_type = set_up ["order_type" ],
463+ side = MarketSide .BUY ,
464+ price = set_up ["price" ],
465+ quantity = set_up ["quantity" ],
466+ tag = "invalid-delivery-area" ,
467+ )
468+ assert excinfo .value .code () == grpc .StatusCode .UNAVAILABLE , "Delivery area not found"
469+
470+
471+ @pytest .mark .asyncio
472+ async def test_create_order_below_minimum_quantity (set_up : dict [str , Any ]) -> None :
473+ """Test creating an order with a quantity below the minimum allowed."""
474+ below_min_quantity = Power (mw = MIN_QUANTITY_MW - Decimal ("0.01" ))
475+ with pytest .raises (ValueError , match = f"Quantity must be at least { MIN_QUANTITY_MW } MW." ):
476+ await create_test_order (set_up , quantity = below_min_quantity )
477+
478+
479+ @pytest .mark .asyncio
480+ async def test_create_order_above_maximum_price (set_up : dict [str , Any ]) -> None :
481+ """Test creating an order with a price above the maximum allowed."""
482+ above_max_price = Price (amount = MAX_PRICE + Decimal ("0.01" ), currency = Currency .EUR )
483+ with pytest .raises (ValueError , match = f"Price must be between { MIN_PRICE } and { MAX_PRICE } ." ):
484+ await create_test_order (set_up , price = above_max_price )
485+
486+
487+ @pytest .mark .asyncio
488+ async def test_create_order_at_maximum_price (set_up : dict [str , Any ]) -> None :
489+ """Test creating an order with the exact maximum allowed price."""
490+ max_price = Price (amount = MAX_PRICE , currency = Currency .EUR )
491+ order = await create_test_order (set_up , price = max_price )
492+ assert order .order .price .amount == max_price .amount , "Order with maximum price was not created correctly"
493+
494+ @pytest .mark .asyncio
495+ async def test_create_order_at_minimum_quantity_and_price (set_up : dict [str , Any ]) -> None :
496+ """Test creating an order with the exact minimum allowed quantity and price."""
497+ min_quantity = Power (mw = MIN_QUANTITY_MW )
498+ min_price = Price (amount = MIN_PRICE , currency = Currency .EUR )
499+ order = await create_test_order (set_up , quantity = min_quantity , price = min_price )
500+ assert order .order .quantity .mw == min_quantity .mw , \
501+ "Order with minimum quantity was not created correctly"
502+ assert order .order .price .amount == min_price .amount , \
503+ "Order with minimum price was not created correctly"
504+
505+
506+ @pytest .mark .asyncio
507+ async def test_update_order_to_invalid_price (set_up : dict [str , Any ]) -> None :
508+ """Test updating an order to have a price outside the valid range."""
509+ order = await create_test_order (set_up )
510+ invalid_price = Price (amount = MAX_PRICE + Decimal ("0.01" ), currency = Currency .EUR )
511+ with pytest .raises (ValueError , match = f"Price must be between { MIN_PRICE } and { MAX_PRICE } ." ):
512+ await set_up ["client" ].update_gridpool_order (
513+ gridpool_id = GRIDPOOL_ID , order_id = order .order_id , price = invalid_price
514+ )
515+
516+
517+
518+
519+ @pytest .mark .asyncio
520+ async def test_concurrent_cancel_and_update_order (set_up : dict [str , Any ]) -> None :
521+ """Test concurrent cancellation and update of the same order."""
522+ order = await create_test_order (set_up )
523+ new_price = Price (amount = Decimal ("50" ), currency = Currency .EUR )
524+
525+ cancelled_order = await set_up ["client" ].cancel_gridpool_order (GRIDPOOL_ID , order .order_id )
526+
527+ with pytest .raises (grpc .aio .AioRpcError ) as excinfo :
528+ await set_up ["client" ].update_gridpool_order (
529+ gridpool_id = GRIDPOOL_ID , order_id = order .order_id , price = new_price
530+ )
531+ assert excinfo .value .code () == grpc .StatusCode .INVALID_ARGUMENT , "Order is already cancelled"
532+
533+
534+ @pytest .mark .asyncio
535+ async def test_multiple_streams_different_filters (set_up : dict [str , Any ]) -> None :
536+ """Test creating multiple streams with different filters and ensure independent operation."""
537+ area_1 = DeliveryArea (code = "10YDE-EON------1" , code_type = EnergyMarketCodeType .EUROPE_EIC )
538+ area_2 = DeliveryArea (code = "10YDE-RWENET---I" , code_type = EnergyMarketCodeType .EUROPE_EIC )
539+
540+ stream_1 = await set_up ["client" ].stream_gridpool_orders (GRIDPOOL_ID , delivery_area = area_1 )
541+ stream_2 = await set_up ["client" ].stream_gridpool_orders (GRIDPOOL_ID , delivery_area = area_2 )
542+
543+ # Create orders in each area to see if they appear on correct streams
544+ order_1 = await create_test_order (set_up , delivery_area = area_1 )
545+ order_2 = await create_test_order (set_up , delivery_area = area_2 )
546+
547+ try :
548+ streamed_order_1 = await asyncio .wait_for (anext (stream_1 ), timeout = 15 )
549+ streamed_order_2 = await asyncio .wait_for (anext (stream_2 ), timeout = 15 )
550+
551+ assert streamed_order_1 .order == order_1 .order , "Streamed order does not match area-specific order in stream 1"
552+ assert streamed_order_2 .order == order_2 .order , "Streamed order does not match area-specific order in stream 2"
553+ except asyncio .TimeoutError :
554+ pytest .fail ("Failed to receive streamed orders within timeout" )
555+
556+
379557
380558@pytest .fixture (scope = "session" )
381559def event_loop () -> Generator [asyncio .AbstractEventLoop , None , None ]:
0 commit comments