diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 7526088..10b9670 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -6,11 +6,12 @@ ## Upgrading - +* Update delivery period time filter validation to remove inappropriate start time check. ## New Features * Add check to validate order details. +* CLI: Support delivery time filter in gridpool streams. ## Bug Fixes diff --git a/src/frequenz/client/electricity_trading/_client.py b/src/frequenz/client/electricity_trading/_client.py index 80743cb..8392c48 100644 --- a/src/frequenz/client/electricity_trading/_client.py +++ b/src/frequenz/client/electricity_trading/_client.py @@ -300,8 +300,10 @@ def gridpool_orders_stream( Raises: grpc.RpcError: If an error occurs while streaming the orders. + ValueError: If an invalid delivery_time_filter is provided. """ - self.validate_params(delivery_time_filter=delivery_time_filter) + if delivery_time_filter and not delivery_time_filter.is_valid: + raise ValueError("Invalid delivery_time_filter provided.") gridpool_order_filter = GridpoolOrderFilter( order_states=order_states, @@ -366,8 +368,10 @@ def gridpool_trades_stream( Raises: grpc.RpcError: If an error occurs while streaming gridpool trades. + ValueError: If an invalid delivery_time_filter is provided. """ - self.validate_params(delivery_time_filter=delivery_time_filter) + if delivery_time_filter and not delivery_time_filter.is_valid: + raise ValueError("Invalid delivery_time_filter provided.") gridpool_trade_filter = GridpoolTradeFilter( trade_states=trade_states, diff --git a/src/frequenz/client/electricity_trading/_types.py b/src/frequenz/client/electricity_trading/_types.py index af97e25..f4ba895 100644 --- a/src/frequenz/client/electricity_trading/_types.py +++ b/src/frequenz/client/electricity_trading/_types.py @@ -1577,6 +1577,25 @@ def to_pb(self) -> electricity_trading_pb2.DeliveryTimeFilter: ), ) + @property + def is_valid(self) -> bool: + """Check if the DeliveryTimeFilter is valid. + + Verifies that the start and end dates in the time interval are logical, if both are set. + + Returns: + True if the filter is valid, False otherwise. + """ + if self.time_interval is None: + return True + + start = self.time_interval.start_time + end = self.time_interval.end_time + if start is None or end is None: + return True + + return start < end + @dataclass(frozen=True) class GridpoolOrderFilter: diff --git a/src/frequenz/client/electricity_trading/cli/__main__.py b/src/frequenz/client/electricity_trading/cli/__main__.py index 1052a22..9c48eff 100644 --- a/src/frequenz/client/electricity_trading/cli/__main__.py +++ b/src/frequenz/client/electricity_trading/cli/__main__.py @@ -51,15 +51,15 @@ def cli() -> None: @click.option("--url", required=True, type=str) @click.option("--auth_key", required=True, type=str) @click.option("--delivery-start", default=None, type=iso) -@click.option("--start", default=None, type=iso) -@click.option("--end", default=None, type=iso) +@click.option("--execution-from", default=None, type=iso) +@click.option("--execution-to", default=None, type=iso) @click.option("--sign_secret", default=None, type=str) def receive_public_trades( # pylint: disable=too-many-arguments url: str, auth_key: str, *, - start: datetime, - end: datetime, + execution_from: datetime, + execution_to: datetime, delivery_start: datetime, sign_secret: str | None = None, ) -> None: @@ -69,8 +69,8 @@ def receive_public_trades( # pylint: disable=too-many-arguments url=url, auth_key=auth_key, delivery_start=delivery_start, - start=start, - end=end, + start=execution_from, + end=execution_to, sign_secret=sign_secret, ) ) @@ -80,15 +80,15 @@ def receive_public_trades( # pylint: disable=too-many-arguments @click.option("--url", required=True, type=str) @click.option("--auth_key", required=True, type=str) @click.option("--delivery-start", default=None, type=iso) -@click.option("--start", default=None, type=iso) -@click.option("--end", default=None, type=iso) +@click.option("--execution-from", default=None, type=iso) +@click.option("--execution-to", default=None, type=iso) @click.option("--sign_secret", default=None, type=str) def receive_public_orders( # pylint: disable=too-many-arguments url: str, auth_key: str, *, - start: datetime, - end: datetime, + execution_from: datetime, + execution_to: datetime, delivery_start: datetime, sign_secret: str | None = None, ) -> None: @@ -98,25 +98,38 @@ def receive_public_orders( # pylint: disable=too-many-arguments url=url, auth_key=auth_key, delivery_start=delivery_start, - start=start, - end=end, + start=execution_from, + end=execution_to, sign_secret=sign_secret, ) ) +# pylint: disable=too-many-arguments @cli.command() @click.option("--url", required=True, type=str) @click.option("--auth_key", required=True, type=str) @click.option("--gid", required=True, type=int) -@click.option("--start", default=None, type=iso) +@click.option( + "--delivery-from", + default=None, + type=iso, + help="Start timestamp (inclusive) to filter delivery start times.", +) +@click.option( + "--delivery-to", + default=None, + type=iso, + help="End timestamp (exclusive) to filter delivery start times.", +) @click.option("--sign_secret", default=None, type=str) def receive_gridpool_trades( url: str, auth_key: str, gid: int, *, - start: datetime, + delivery_from: datetime | None, + delivery_to: datetime | None, sign_secret: str | None = None, ) -> None: """List and/or stream gridpool trades.""" @@ -125,23 +138,37 @@ def receive_gridpool_trades( url=url, auth_key=auth_key, gid=gid, - delivery_start=start, + delivery_from=delivery_from, + delivery_to=delivery_to, sign_secret=sign_secret, ) ) +# pylint: disable=too-many-arguments @cli.command() @click.option("--url", required=True, type=str) @click.option("--auth_key", required=True, type=str) -@click.option("--start", default=None, type=iso) +@click.option( + "--delivery-from", + default=None, + type=iso, + help="Start timestamp (inclusive) to filter delivery start times.", +) +@click.option( + "--delivery-to", + default=None, + type=iso, + help="End timestamp (exclusive) to filter delivery start times.", +) @click.option("--gid", required=True, type=int) @click.option("--sign_secret", default=None, type=str) def receive_gridpool_orders( url: str, auth_key: str, *, - start: datetime, + delivery_from: datetime | None, + delivery_to: datetime | None, gid: int, sign_secret: str | None = None, ) -> None: @@ -150,7 +177,8 @@ def receive_gridpool_orders( run_list_gridpool_orders( url=url, auth_key=auth_key, - delivery_start=start, + delivery_from=delivery_from, + delivery_to=delivery_to, gid=gid, sign_secret=sign_secret, ) diff --git a/src/frequenz/client/electricity_trading/cli/etrading.py b/src/frequenz/client/electricity_trading/cli/etrading.py index 6b0f770..9193484 100644 --- a/src/frequenz/client/electricity_trading/cli/etrading.py +++ b/src/frequenz/client/electricity_trading/cli/etrading.py @@ -4,7 +4,7 @@ """CLI tool to interact with the trading API.""" from collections import deque -from datetime import datetime, timedelta, timezone +from datetime import datetime, timedelta from decimal import Decimal from enum import Enum from typing import AsyncIterator @@ -125,68 +125,64 @@ async def receive_public_orders( # pylint: disable=too-many-arguments print_public_order(order) +# pylint: disable=too-many-arguments async def list_gridpool_trades( url: str, auth_key: str, gid: int, *, - delivery_start: datetime, + delivery_from: datetime | None, + delivery_to: datetime | None, sign_secret: str | None = None, ) -> None: """List gridpool trades and stream new gridpool trades. - Optionally a delivery_start can be provided to filter the trades by delivery period. + Optionally trades can be filtered by delivery period. Args: url: URL of the trading API. auth_key: API key. gid: Gridpool ID. - delivery_start: Start of the delivery period or None. + delivery_from: Start timestamp (inclusive) to filter delivery start times or None. + delivery_to: End timestamp (exclusive) to filter delivery start times or None. sign_secret: The cryptographic secret to use for HMAC generation. """ client = Client(server_url=url, auth_key=auth_key, sign_secret=sign_secret) print_trade_header() - delivery_time_filter = None - # If delivery period is selected, list historical trades also - if delivery_start is not None: - check_delivery_start(delivery_start) - delivery_time_filter = DeliveryTimeFilter( - time_interval=Interval( - start_time=delivery_start, - end_time=delivery_start + timedelta(minutes=15), - ), - duration_filters=[], - ) - lst = client.list_gridpool_trades(gid, delivery_time_filter=delivery_time_filter) - - async for trade in lst: - print_trade(trade) - - if delivery_start and delivery_start <= datetime.now(timezone.utc): - return + delivery_time_filter = DeliveryTimeFilter(Interval(delivery_from, delivery_to)) + lst = client.list_gridpool_trades( + gid, + delivery_time_filter=delivery_time_filter, + ) + # Initialize the stream before printing to minimize the gap between the two stream = client.gridpool_trades_stream( - gid, delivery_time_filter=delivery_time_filter + gid, + delivery_time_filter=delivery_time_filter, ).new_receiver() + + async for trade in lst: + print_trade(trade, gid) + async for trade in stream: - print_trade(trade) + print_trade(trade, gid) +# pylint: disable=too-many-arguments async def list_gridpool_orders( url: str, auth_key: str, *, - delivery_start: datetime, + delivery_from: datetime | None, + delivery_to: datetime | None, gid: int, sign_secret: str | None = None, ) -> None: """List orders and stream new gridpool orders. - If delivery_start is provided, list historical orders and stream new orders - for the 15 minute delivery period starting at delivery_start. - If no delivery_start is provided, stream new orders for any delivery period. + Optionally orders can be filtered by delivery period. Note that retrieved sort order for listed orders (starting from the newest) is reversed in chunks trying to bring more recent orders to the bottom. @@ -194,7 +190,8 @@ async def list_gridpool_orders( Args: url: URL of the trading API. auth_key: API key. - delivery_start: Start of the delivery period or None. + delivery_from: Start timestamp (inclusive) to filter delivery start times or None. + delivery_to: End timestamp (exclusive) to filter delivery start times or None. gid: Gridpool ID. sign_secret: The cryptographic secret to use for HMAC generation. """ @@ -202,30 +199,23 @@ async def list_gridpool_orders( print_order_header() - delivery_time_filter = None - # If delivery period is selected, list historical orders also - if delivery_start is not None: - check_delivery_start(delivery_start) - delivery_time_filter = DeliveryTimeFilter( - time_interval=Interval( - start_time=delivery_start, - end_time=delivery_start + timedelta(minutes=15), - ), - duration_filters=[], - ) - lst = client.list_gridpool_orders(gid, delivery_time_filter=delivery_time_filter) - - async for order in reverse_iterator(lst): - print_order(order) + delivery_time_filter = DeliveryTimeFilter(Interval(delivery_from, delivery_to)) - if delivery_start and delivery_start <= datetime.now(timezone.utc): - return + lst = client.list_gridpool_orders( + gid, + delivery_time_filter=delivery_time_filter, + ) stream = client.gridpool_orders_stream( - gid, delivery_time_filter=delivery_time_filter + gid, + delivery_time_filter=delivery_time_filter, ).new_receiver() + + async for order in reverse_iterator(lst): + print_order(order, gid) + async for order in stream: - print_order(order) + print_order(order, gid) # pylint: disable=too-many-arguments @@ -287,7 +277,7 @@ async def create_order( tag=tag, ) - print_order(order) + print_order(order, gid) async def cancel_order( @@ -406,12 +396,13 @@ def print_trade_header() -> None: "quantity_mw," "currency," "price," - "state " + "state," + "gridpool_id" ) print(header) -def print_trade(trade: Trade) -> None: +def print_trade(trade: Trade, gid: int) -> None: """Print trade details to stdout in CSV format.""" values = ( trade.id, @@ -426,6 +417,7 @@ def print_trade(trade: Trade) -> None: trade.price.currency, trade.price.amount, trade.state, + gid, ) print(",".join(v.name if isinstance(v, Enum) else str(v) for v in values)) @@ -447,12 +439,13 @@ def print_order_header() -> None: "currency," "price," "state," - "tag" + "tag," + "gridpool_id" ) print(header) -def print_order(order: OrderDetail) -> None: +def print_order(order: OrderDetail, gid: int) -> None: """ Print order details to stdout in CSV format. @@ -469,6 +462,7 @@ def print_order(order: OrderDetail) -> None: Args: order: OrderDetail object + gid: Gridpool ID """ values = [ order.order_id, @@ -486,6 +480,7 @@ def print_order(order: OrderDetail) -> None: order.order.price.amount, order.state_detail.state, order.order.tag, + gid, ] print(",".join(v.name if isinstance(v, Enum) else str(v) for v in values))