-
Notifications
You must be signed in to change notification settings - Fork 11
Introduces CLI commands to interact with the trading API: #96
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,41 +3,75 @@ | |
|
|
||
| """CLI tool to interact with the trading API.""" | ||
|
|
||
| import argparse | ||
| import asyncio | ||
| from datetime import datetime, timedelta | ||
| from zoneinfo import ZoneInfo | ||
|
|
||
| import click | ||
|
|
||
| from frequenz.client.electricity_trading.cli.day_ahead import list_day_ahead_prices | ||
| from frequenz.client.electricity_trading.cli.etrading import ( | ||
| list_orders as run_list_orders, | ||
| ) | ||
| from frequenz.client.electricity_trading.cli.etrading import ( | ||
| list_trades as run_list_trades, | ||
| ) | ||
|
|
||
| TZ = ZoneInfo("Europe/Berlin") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we should take it from the "TZ" environment variable and use Berlin as the default, in case we want to call the command with UTC, etc., for whatever reason.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wanted to keep this since this is the timezone used on EEX and EPEX, which is what is supported for day-ahead prices. |
||
|
|
||
| def main() -> None: | ||
| """Run main entry point for the CLI tool.""" | ||
| tz = ZoneInfo("Europe/Berlin") | ||
| midnight = datetime.combine(datetime.now(tz), datetime.min.time(), tzinfo=tz) | ||
| parser = argparse.ArgumentParser() | ||
| parser.add_argument("--entsoe_key", type=str, required=True) | ||
| parser.add_argument( | ||
| "--start", | ||
| type=datetime.fromisoformat, | ||
| required=False, | ||
| default=midnight, | ||
| ) | ||
| parser.add_argument( | ||
| "--end", | ||
| type=datetime.fromisoformat, | ||
| required=False, | ||
| default=midnight + timedelta(days=2), | ||
| ) | ||
| parser.add_argument("--country_code", type=str, required=False, default="DE_LU") | ||
| args = parser.parse_args() | ||
| iso = datetime.fromisoformat | ||
|
|
||
|
|
||
| def midnight(days: int = 0) -> str: | ||
| """Return today's midnight.""" | ||
| return ( | ||
| datetime.combine(datetime.now(TZ), datetime.min.time(), tzinfo=TZ) | ||
| + timedelta(days) | ||
| ).isoformat() | ||
|
|
||
|
|
||
| @click.group() | ||
| def cli() -> None: | ||
| """CLI tool to interact with the trading API.""" | ||
|
|
||
|
|
||
| @cli.command() | ||
| @click.option("--url", required=True, type=str) | ||
| @click.option("--key", required=True, type=str) | ||
| @click.option("--start", default=None, type=iso) | ||
| def list_trades(url: str, key: str, *, start: datetime) -> None: | ||
| """List trades.""" | ||
| asyncio.run(run_list_trades(url=url, key=key, delivery_start=start)) | ||
|
|
||
|
|
||
| @cli.command() | ||
| @click.option("--url", required=True, type=str) | ||
| @click.option("--key", required=True, type=str) | ||
| @click.option("--start", default=None, type=iso) | ||
| @click.option("--gid", required=True, type=int) | ||
| def list_orders(url: str, key: str, *, start: datetime, gid: int) -> None: | ||
| """List orders.""" | ||
| asyncio.run(run_list_orders(url=url, key=key, delivery_start=start, gid=gid)) | ||
|
|
||
|
|
||
| @cli.command() | ||
| @click.option("--entsoe-key", required=True, type=str) | ||
| @click.option("--start", default=midnight(), type=iso) | ||
| @click.option("--end", default=midnight(days=2), type=iso) | ||
| @click.option("--country-code", type=str, default="DE_LU") | ||
| def list_day_ahead( | ||
| entsoe_key: str, *, start: datetime, end: datetime, country_code: str | ||
| ) -> None: | ||
| """List day-ahead prices.""" | ||
| list_day_ahead_prices( | ||
| entsoe_key=args.entsoe_key, | ||
| start=args.start, | ||
| end=args.end, | ||
| country_code=args.country_code, | ||
| entsoe_key=entsoe_key, start=start, end=end, country_code=country_code | ||
| ) | ||
|
|
||
|
|
||
| def main() -> None: | ||
| """Run the main Click CLI.""" | ||
| cli() | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,204 @@ | ||
| # License: MIT | ||
| # Copyright © 2025 Frequenz Energy-as-a-Service GmbH | ||
|
|
||
| """CLI tool to interact with the trading API.""" | ||
|
|
||
| from datetime import datetime, timedelta, timezone | ||
| from enum import Enum | ||
|
|
||
| from frequenz.client.electricity_trading import ( | ||
| Client, | ||
| DeliveryPeriod, | ||
| OrderDetail, | ||
| PublicTrade, | ||
| ) | ||
|
|
||
|
|
||
| def check_delivery_start( | ||
| ts: datetime, duration: timedelta = timedelta(minutes=15) | ||
| ) -> None: | ||
| """Validate that the delivery start is a multiple of duration. | ||
|
|
||
| Args: | ||
| ts: Delivery start timestamp. | ||
| duration: Delivery period duration. | ||
|
|
||
| Raises: | ||
| ValueError: If `ts` is not a multiple of `duration`. | ||
| """ | ||
| if int(ts.timestamp()) % int(duration.total_seconds()) != 0: | ||
| raise ValueError("Delivery period must be a multiple of `duration`.") | ||
|
|
||
|
|
||
| async def list_trades(url: str, key: str, *, delivery_start: datetime) -> None: | ||
| """List trades and stream new trades. | ||
|
|
||
| If delivery_start is provided, list historical trades and stream new trades | ||
| for the 15 minute delivery period starting at delivery_start. | ||
| If no delivery_start is provided, stream new trades for any delivery period. | ||
|
|
||
| Args: | ||
| url: URL of the trading API. | ||
| key: API key. | ||
| delivery_start: Start of the delivery period or None. | ||
| """ | ||
| client = Client(server_url=url, auth_key=key) | ||
|
|
||
| print_trade_header() | ||
|
|
||
| delivery_period = None | ||
| # If delivery period is selected, list historical trades also | ||
| if delivery_start is not None: | ||
| check_delivery_start(delivery_start) | ||
| delivery_period = DeliveryPeriod( | ||
| start=delivery_start, | ||
| duration=timedelta(minutes=15), | ||
| ) | ||
| lst = client.list_public_trades(delivery_period=delivery_period) | ||
|
|
||
| async for trade in lst: | ||
| print_trade(trade) | ||
|
|
||
| if delivery_start <= datetime.now(timezone.utc): | ||
| return | ||
|
|
||
| stream = await client.stream_public_trades(delivery_period=delivery_period) | ||
| async for trade in stream: | ||
| print_trade(trade) | ||
|
|
||
|
|
||
| async def list_orders( | ||
| url: str, key: str, *, delivery_start: datetime, gid: int | ||
| ) -> None: | ||
| """List orders and stream new 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. | ||
|
|
||
| Args: | ||
| url: URL of the trading API. | ||
| key: API key. | ||
| delivery_start: Start of the delivery period or None. | ||
| gid: Gridpool ID. | ||
| """ | ||
| client = Client(server_url=url, auth_key=key) | ||
|
|
||
| # print_header() | ||
|
|
||
| delivery_period = None | ||
| # If delivery period is selected, list historical orders also | ||
| if delivery_start is not None: | ||
| check_delivery_start(delivery_start) | ||
| delivery_period = DeliveryPeriod( | ||
| start=delivery_start, | ||
| duration=timedelta(minutes=15), | ||
| ) | ||
| lst = client.list_gridpool_orders(gid, delivery_period=delivery_period) | ||
|
|
||
| async for order in lst: | ||
| print_order(order) | ||
|
|
||
| if delivery_start and delivery_start <= datetime.now(timezone.utc): | ||
| return | ||
|
|
||
| stream = await client.stream_gridpool_orders(gid, delivery_period=delivery_period) | ||
| async for order in stream: | ||
| print_order(order) | ||
|
|
||
|
|
||
| def print_trade_header() -> None: | ||
| """Print trade header in CSV format.""" | ||
| header = ( | ||
| "public_trade_id, " | ||
| "execution_time, " | ||
| "delivery_period_start, " | ||
| "delivery_period_duration, " | ||
| "buy_delivery_area_code, " | ||
| "sell_delivery_area_code, " | ||
| "buy_delivery_area_code_type, " | ||
| "sell_delivery_area_code_type" | ||
| "quantity_mw, " | ||
| "price, " | ||
| "currency, " | ||
| "state, " | ||
| ) | ||
| print(header) | ||
|
|
||
|
|
||
| def print_trade(trade: PublicTrade) -> None: | ||
| """Print trade details to stdout in CSV format.""" | ||
| values = ( | ||
| trade.public_trade_id, | ||
| trade.execution_time.isoformat(), | ||
| trade.delivery_period.start.isoformat(), | ||
| trade.delivery_period.duration, | ||
| trade.buy_delivery_area.code, | ||
| trade.sell_delivery_area.code, | ||
| trade.buy_delivery_area.code_type, | ||
| trade.sell_delivery_area.code_type, | ||
| trade.quantity.mw, | ||
| trade.price.currency, | ||
| trade.price.amount, | ||
| trade.state, | ||
| ) | ||
| print(",".join(v.name if isinstance(v, Enum) else str(v) for v in values)) | ||
|
|
||
|
|
||
| def print_order_header() -> None: | ||
| """Print order header in CSV format.""" | ||
| header = ( | ||
| "order_id, " | ||
| "create_time, " | ||
| "modification_time, " | ||
| "delivery_period_start, " | ||
| "delivery_period_duration" | ||
| "delivery_area_code, " | ||
| "delivery_area_code_type, " | ||
| "order_type, " | ||
| "quantity_mw, " | ||
| "open_quantity_mw, " | ||
| "side, " | ||
| "currency, " | ||
| "price, " | ||
| "state, " | ||
| ) | ||
| print(header) | ||
|
|
||
|
|
||
| def print_order(order: OrderDetail) -> None: | ||
| """ | ||
| Print order details to stdout in CSV format. | ||
|
|
||
| All fields except the following are printed: | ||
| - order.stop_price | ||
| - order.peak_price_delta | ||
| - order.display_quantity | ||
| - order.execution_option | ||
| - order.valid_until | ||
| - order.payload | ||
| - order.tag | ||
| - state_detail.state_reason | ||
| - state_detail.market_actor | ||
| - filled_quantity | ||
|
|
||
| Args: | ||
| order: OrderDetail object | ||
| """ | ||
| values = [ | ||
| order.order_id, | ||
| order.create_time.isoformat(), | ||
| order.modification_time.isoformat(), | ||
| order.order.delivery_period.start.isoformat(), | ||
| order.order.delivery_period.duration, | ||
| order.order.delivery_area.code, | ||
| order.order.delivery_area.code_type, | ||
| order.order.type, | ||
| order.order.quantity.mw, | ||
| order.open_quantity.mw, | ||
| order.order.side, | ||
| order.order.price.currency, | ||
| order.order.price.amount, | ||
| order.state_detail.state, | ||
| ] | ||
| print(",".join(v.name if isinstance(v, Enum) else str(v) for v in values)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs to go in
pyproject.tomlas well, I think.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.