Skip to content

Commit 94f5ed4

Browse files
committed
Add CLI commands to list trades and orders via API
Introduces CLI commands to interact with the trading API: - `list_trades` allows listing and streaming public trades for specified delivery periods. If no delivery start is given, starts streaming all new public trades. - `list_orders` allows listing and streaming orders for specified delivery periods and gridpool IDs. If no delivery start is given, starts streaming all new orders for this gridpool ID. Signed-off-by: cwasicki <[email protected]>
1 parent fa862ad commit 94f5ed4

File tree

2 files changed

+230
-0
lines changed

2 files changed

+230
-0
lines changed

src/frequenz/client/electricity_trading/cli/__main__.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,19 @@
33

44
"""CLI tool to interact with the trading API."""
55

6+
import asyncio
67
from datetime import datetime, timedelta
78
from zoneinfo import ZoneInfo
89

910
import click
1011

1112
from frequenz.client.electricity_trading.cli.day_ahead import list_day_ahead_prices
13+
from frequenz.client.electricity_trading.cli.etrading import (
14+
list_orders as run_list_orders,
15+
)
16+
from frequenz.client.electricity_trading.cli.etrading import (
17+
list_trades as run_list_trades,
18+
)
1219

1320
TZ = ZoneInfo("Europe/Berlin")
1421

@@ -28,6 +35,25 @@ def cli() -> None:
2835
"""CLI tool to interact with the trading API."""
2936

3037

38+
@cli.command()
39+
@click.option("--url", required=True, type=str)
40+
@click.option("--key", required=True, type=str)
41+
@click.option("--start", default=None, type=iso)
42+
def list_trades(url: str, key: str, *, start: datetime) -> None:
43+
"""List trades."""
44+
asyncio.run(run_list_trades(url=url, key=key, delivery_start=start))
45+
46+
47+
@cli.command()
48+
@click.option("--url", required=True, type=str)
49+
@click.option("--key", required=True, type=str)
50+
@click.option("--start", default=None, type=iso)
51+
@click.option("--gid", required=True, type=int)
52+
def list_orders(url: str, key: str, *, start: datetime, gid: int) -> None:
53+
"""List orders."""
54+
asyncio.run(run_list_orders(url=url, key=key, delivery_start=start, gid=gid))
55+
56+
3157
@cli.command()
3258
@click.option("--entsoe-key", required=True, type=str)
3359
@click.option("--start", default=midnight(), type=iso)
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
# License: MIT
2+
# Copyright © 2025 Frequenz Energy-as-a-Service GmbH
3+
4+
"""CLI tool to interact with the trading API."""
5+
6+
from datetime import datetime, timedelta, timezone
7+
from enum import Enum
8+
9+
from frequenz.client.electricity_trading import (
10+
Client,
11+
DeliveryPeriod,
12+
OrderDetail,
13+
PublicTrade,
14+
)
15+
16+
17+
def check_delivery_start(
18+
ts: datetime, duration: timedelta = timedelta(minutes=15)
19+
) -> None:
20+
"""Validate that the delivery start is a multiple of duration.
21+
22+
Args:
23+
ts: Delivery start timestamp.
24+
duration: Delivery period duration.
25+
26+
Raises:
27+
ValueError: If `ts` is not a multiple of `duration`.
28+
"""
29+
if int(ts.timestamp()) % int(duration.total_seconds()) != 0:
30+
raise ValueError("Delivery period must be a multiple of `duration`.")
31+
32+
33+
async def list_trades(url: str, key: str, *, delivery_start: datetime) -> None:
34+
"""List trades and stream new trades.
35+
36+
If delivery_start is provided, list historical trades and stream new trades
37+
for the 15 minute delivery period starting at delivery_start.
38+
If no delivery_start is provided, stream new trades for any delivery period.
39+
40+
Args:
41+
url: URL of the trading API.
42+
key: API key.
43+
delivery_start: Start of the delivery period or None.
44+
"""
45+
client = Client(server_url=url, auth_key=key)
46+
47+
print_trade_header()
48+
49+
delivery_period = None
50+
# If delivery period is selected, list historical trades also
51+
if delivery_start is not None:
52+
check_delivery_start(delivery_start)
53+
delivery_period = DeliveryPeriod(
54+
start=delivery_start,
55+
duration=timedelta(minutes=15),
56+
)
57+
lst = client.list_public_trades(delivery_period=delivery_period)
58+
59+
async for trade in lst:
60+
print_trade(trade)
61+
62+
if delivery_start <= datetime.now(timezone.utc):
63+
return
64+
65+
stream = await client.stream_public_trades(delivery_period=delivery_period)
66+
async for trade in stream:
67+
print_trade(trade)
68+
69+
70+
async def list_orders(
71+
url: str, key: str, *, delivery_start: datetime, gid: int
72+
) -> None:
73+
"""List orders and stream new orders.
74+
75+
If delivery_start is provided, list historical orders and stream new orders
76+
for the 15 minute delivery period starting at delivery_start.
77+
If no delivery_start is provided, stream new orders for any delivery period.
78+
79+
Args:
80+
url: URL of the trading API.
81+
key: API key.
82+
delivery_start: Start of the delivery period or None.
83+
gid: Gridpool ID.
84+
"""
85+
client = Client(server_url=url, auth_key=key)
86+
87+
# print_header()
88+
89+
delivery_period = None
90+
# If delivery period is selected, list historical orders also
91+
if delivery_start is not None:
92+
check_delivery_start(delivery_start)
93+
delivery_period = DeliveryPeriod(
94+
start=delivery_start,
95+
duration=timedelta(minutes=15),
96+
)
97+
lst = client.list_gridpool_orders(gid, delivery_period=delivery_period)
98+
99+
async for order in lst:
100+
print_order(order)
101+
102+
if delivery_start and delivery_start <= datetime.now(timezone.utc):
103+
return
104+
105+
stream = await client.stream_gridpool_orders(gid, delivery_period=delivery_period)
106+
async for order in stream:
107+
print_order(order)
108+
109+
110+
def print_trade_header() -> None:
111+
"""Print trade header in CSV format."""
112+
header = (
113+
"public_trade_id, "
114+
"execution_time, "
115+
"delivery_period_start, "
116+
"delivery_period_duration, "
117+
"buy_delivery_area_code, "
118+
"sell_delivery_area_code, "
119+
"buy_delivery_area_code_type, "
120+
"sell_delivery_area_code_type"
121+
"quantity_mw, "
122+
"price, "
123+
"currency, "
124+
"state, "
125+
)
126+
print(header)
127+
128+
129+
def print_trade(trade: PublicTrade) -> None:
130+
"""Print trade details to stdout in CSV format."""
131+
values = (
132+
trade.public_trade_id,
133+
trade.execution_time.isoformat(),
134+
trade.delivery_period.start.isoformat(),
135+
trade.delivery_period.duration,
136+
trade.buy_delivery_area.code,
137+
trade.sell_delivery_area.code,
138+
trade.buy_delivery_area.code_type,
139+
trade.sell_delivery_area.code_type,
140+
trade.quantity.mw,
141+
trade.price.currency,
142+
trade.price.amount,
143+
trade.state,
144+
)
145+
print(",".join(v.name if isinstance(v, Enum) else str(v) for v in values))
146+
147+
148+
def print_order_header() -> None:
149+
"""Print order header in CSV format."""
150+
header = (
151+
"order_id, "
152+
"create_time, "
153+
"modification_time, "
154+
"delivery_period_start, "
155+
"delivery_period_duration"
156+
"delivery_area_code, "
157+
"delivery_area_code_type, "
158+
"order_type, "
159+
"quantity_mw, "
160+
"open_quantity_mw, "
161+
"side, "
162+
"currency, "
163+
"price, "
164+
"state, "
165+
)
166+
print(header)
167+
168+
169+
def print_order(order: OrderDetail) -> None:
170+
"""
171+
Print order details to stdout in CSV format.
172+
173+
All fields except the following are printed:
174+
- order.stop_price
175+
- order.peak_price_delta
176+
- order.display_quantity
177+
- order.execution_option
178+
- order.valid_until
179+
- order.payload
180+
- order.tag
181+
- state_detail.state_reason
182+
- state_detail.market_actor
183+
- filled_quantity
184+
185+
Args:
186+
order: OrderDetail object
187+
"""
188+
values = [
189+
order.order_id,
190+
order.create_time.isoformat(),
191+
order.modification_time.isoformat(),
192+
order.order.delivery_period.start.isoformat(),
193+
order.order.delivery_period.duration,
194+
order.order.delivery_area.code,
195+
order.order.delivery_area.code_type,
196+
order.order.type,
197+
order.order.quantity.mw,
198+
order.open_quantity.mw,
199+
order.order.side,
200+
order.order.price.currency,
201+
order.order.price.amount,
202+
order.state_detail.state,
203+
]
204+
print(",".join(v.name if isinstance(v, Enum) else str(v) for v in values))

0 commit comments

Comments
 (0)