nado-protocol 0.1.4__py3-none-any.whl → 0.1.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
- from typing import Optional
1
+ from typing import Optional, List, Union
2
+ from enum import Enum
2
3
 
3
4
  from pydantic import validator
4
5
 
@@ -9,6 +10,22 @@ from nado_protocol.utils.execute import BaseParams, SignatureParams
9
10
  from nado_protocol.utils.model import NadoBaseModel
10
11
 
11
12
 
13
+ class TriggerType(str, Enum):
14
+ PRICE_TRIGGER = "price_trigger"
15
+ TIME_TRIGGER = "time_trigger"
16
+
17
+
18
+ class TriggerOrderStatusType(str, Enum):
19
+ CANCELLED = "cancelled"
20
+ TRIGGERED = "triggered"
21
+ INTERNAL_ERROR = "internal_error"
22
+ TRIGGERING = "triggering"
23
+ WAITING_PRICE = "waiting_price"
24
+ WAITING_DEPENDENCY = "waiting_dependency"
25
+ TWAP_EXECUTING = "twap_executing"
26
+ TWAP_COMPLETED = "twap_completed"
27
+
28
+
12
29
  class ListTriggerOrdersTx(BaseParams):
13
30
  recvTime: int
14
31
 
@@ -20,18 +37,114 @@ class ListTriggerOrdersParams(NadoBaseModel):
20
37
 
21
38
  type = "list_trigger_orders"
22
39
  tx: ListTriggerOrdersTx
23
- product_id: Optional[int]
24
- pending: bool
25
- max_update_time: Optional[str]
26
- max_digest: Optional[str]
27
- digests: Optional[list[str]]
28
- limit: Optional[int]
29
- signature: Optional[str]
40
+ product_ids: Optional[List[int]] = None
41
+ trigger_types: Optional[List[TriggerType]] = None
42
+ status_types: Optional[List[TriggerOrderStatusType]] = None
43
+ max_update_time: Optional[int] = None
44
+ max_digest: Optional[str] = None
45
+ digests: Optional[List[str]] = None
46
+ reduce_only: Optional[bool] = None
47
+ limit: Optional[int] = None
48
+ signature: Optional[str] = None
49
+
50
+
51
+ class ListTwapExecutionsParams(NadoBaseModel):
52
+ """
53
+ Parameters for listing TWAP executions for a specific order
54
+ """
55
+
56
+ type = "list_twap_executions"
57
+ digest: str
58
+
59
+
60
+ class ExecutedStatusData(NadoBaseModel):
61
+ """Data for executed TWAP execution"""
62
+
63
+ executed_time: int
64
+ execute_response: dict # ExecuteResponse from engine
65
+
66
+
67
+ class ExecutedStatus(NadoBaseModel):
68
+ """Status when TWAP execution has been executed"""
69
+
70
+ executed: ExecutedStatusData
71
+
72
+
73
+ class FailedStatus(NadoBaseModel):
74
+ """Status when TWAP execution failed"""
75
+
76
+ failed: str
77
+
78
+
79
+ class CancelledStatus(NadoBaseModel):
80
+ """Status when TWAP execution was cancelled"""
81
+
82
+ cancelled: str
83
+
84
+
85
+ class TwapExecutionDetail(NadoBaseModel):
86
+ """Detail of a single TWAP execution"""
87
+
88
+ execution_id: int
89
+ scheduled_time: int
90
+ status: Union[
91
+ ExecutedStatus, FailedStatus, CancelledStatus, str
92
+ ] # str for "pending"
93
+ updated_at: int
94
+
95
+
96
+ class TwapExecutionsData(NadoBaseModel):
97
+ """Data model for TWAP executions"""
98
+
99
+ executions: List[TwapExecutionDetail]
100
+
101
+
102
+ class TriggeredStatus(NadoBaseModel):
103
+ """Status when order has been triggered"""
104
+
105
+ triggered: dict # Contains trigger execution details
106
+
107
+
108
+ class TriggerCancelledStatus(NadoBaseModel):
109
+ """Status when order has been cancelled"""
110
+
111
+ cancelled: str # Cancellation reason (e.g., "user_requested")
112
+
113
+
114
+ class TriggerInternalErrorStatus(NadoBaseModel):
115
+ """Status when there was an internal error"""
116
+
117
+ internal_error: str # Error description
118
+
119
+
120
+ class TwapExecutingStatusObject(NadoBaseModel):
121
+ """Status when TWAP order is executing"""
122
+
123
+ twap_executing: dict # Contains execution details
124
+
125
+
126
+ class TwapCompletedStatusObject(NadoBaseModel):
127
+ """Status when TWAP order is completed"""
128
+
129
+ twap_completed: dict # Contains completion details
130
+
131
+
132
+ # Union type for trigger order status
133
+ # Order matters: more specific types (with required fields) should come first
134
+ TriggerOrderStatus = Union[
135
+ TriggeredStatus,
136
+ TriggerCancelledStatus,
137
+ TriggerInternalErrorStatus,
138
+ TwapExecutingStatusObject,
139
+ TwapCompletedStatusObject,
140
+ str, # For simple status strings like "waiting_price", "waiting_dependency", etc.
141
+ ]
30
142
 
31
143
 
32
144
  class TriggerOrder(NadoBaseModel):
33
145
  order: TriggerOrderData
34
- status: str
146
+ status: TriggerOrderStatus
147
+ placed_at: int
35
148
  updated_at: int
36
149
 
37
150
 
@@ -40,7 +153,7 @@ class TriggerOrdersData(NadoBaseModel):
40
153
  Data model for trigger orders
41
154
  """
42
155
 
43
- orders: list[TriggerOrder]
156
+ orders: List[TriggerOrder]
44
157
 
45
158
 
46
159
  class ListTriggerOrdersRequest(ListTriggerOrdersParams):
@@ -54,6 +167,15 @@ class ListTriggerOrdersRequest(ListTriggerOrdersParams):
54
167
  return v
55
168
 
56
169
 
170
+ class ListTwapExecutionsRequest(ListTwapExecutionsParams):
171
+ pass
172
+
173
+
174
+ TriggerQueryParams = Union[ListTriggerOrdersParams, ListTwapExecutionsParams]
175
+ TriggerQueryRequest = Union[ListTriggerOrdersRequest, ListTwapExecutionsRequest]
176
+ TriggerQueryData = Union[TriggerOrdersData, TwapExecutionsData]
177
+
178
+
57
179
  class TriggerQueryResponse(NadoBaseModel):
58
180
  """
59
181
  Represents a response to a query request.
@@ -61,7 +183,7 @@ class TriggerQueryResponse(NadoBaseModel):
61
183
  Attributes:
62
184
  status (ResponseStatus): The status of the query response.
63
185
 
64
- data (Optional[QueryResponseData]): The data returned from the query, or an error message if the query failed.
186
+ data (Optional[TriggerQueryData]): The data returned from the query, or an error message if the query failed.
65
187
 
66
188
  error (Optional[str]): The error message, if any error occurred during the query.
67
189
 
@@ -71,7 +193,7 @@ class TriggerQueryResponse(NadoBaseModel):
71
193
  """
72
194
 
73
195
  status: ResponseStatus
74
- data: Optional[TriggerOrdersData]
75
- error: Optional[str]
76
- error_code: Optional[int]
77
- request_type: Optional[str]
196
+ data: Optional[TriggerQueryData] = None
197
+ error: Optional[str] = None
198
+ error_code: Optional[int] = None
199
+ request_type: Optional[str] = None
@@ -0,0 +1,189 @@
1
+ from typing import List, Optional
2
+ from nado_protocol.utils.order import (
3
+ build_appendix,
4
+ OrderAppendixTriggerType,
5
+ )
6
+ from nado_protocol.utils.expiration import OrderType
7
+ from nado_protocol.utils.execute import OrderParams
8
+
9
+
10
+ def create_twap_order(
11
+ product_id: int,
12
+ sender: str,
13
+ price_x18: str,
14
+ total_amount_x18: str,
15
+ expiration: int,
16
+ nonce: int,
17
+ times: int,
18
+ slippage_frac: float,
19
+ interval_seconds: int,
20
+ custom_amounts_x18: Optional[List[str]] = None,
21
+ reduce_only: bool = False,
22
+ spot_leverage: Optional[bool] = None,
23
+ id: Optional[int] = None,
24
+ ):
25
+ """
26
+ Create a TWAP (Time-Weighted Average Price) order.
27
+
28
+ Args:
29
+ product_id (int): The product ID for the order.
30
+ sender (str): The sender address (32 bytes hex).
31
+ price_x18 (str): The limit price multiplied by 1e18.
32
+ total_amount_x18 (str): The total amount to trade multiplied by 1e18 (signed, negative for sell).
33
+ expiration (int): Order expiration timestamp.
34
+ nonce (int): Order nonce.
35
+ times (int): Number of TWAP executions (1-500).
36
+ slippage_frac (float): Slippage tolerance as a fraction (e.g., 0.01 for 1%).
37
+ interval_seconds (int): Time interval between executions in seconds.
38
+ custom_amounts_x18 (Optional[List[str]]): Custom amounts for each execution multiplied by 1e18.
39
+ If provided, uses TWAP_CUSTOM_AMOUNTS trigger type.
40
+ reduce_only (bool): Whether this is a reduce-only order.
41
+ spot_leverage (Optional[bool]): Whether to use spot leverage.
42
+ id (Optional[int]): Optional order ID.
43
+
44
+ Returns:
45
+ PlaceTriggerOrderParams: Parameters for placing the TWAP order.
46
+
47
+ Raises:
48
+ ValueError: If parameters are invalid.
49
+ """
50
+ # Import here to avoid circular imports
51
+ from nado_protocol.trigger_client.types.models import TimeTrigger
52
+ from nado_protocol.trigger_client.types.execute import PlaceTriggerOrderParams
53
+
54
+ if times < 1 or times > 500:
55
+ raise ValueError(f"TWAP times must be between 1 and 500, got {times}")
56
+
57
+ if slippage_frac < 0 or slippage_frac > 1:
58
+ raise ValueError(
59
+ f"Slippage fraction must be between 0 and 1, got {slippage_frac}"
60
+ )
61
+
62
+ if interval_seconds <= 0:
63
+ raise ValueError(f"Interval must be positive, got {interval_seconds}")
64
+
65
+ # Determine trigger type
66
+ trigger_type = (
67
+ OrderAppendixTriggerType.TWAP_CUSTOM_AMOUNTS
68
+ if custom_amounts_x18 is not None
69
+ else OrderAppendixTriggerType.TWAP
70
+ )
71
+
72
+ # Build appendix - TWAP orders must use IOC execution type
73
+ appendix = build_appendix(
74
+ order_type=OrderType.IOC,
75
+ reduce_only=reduce_only,
76
+ trigger_type=trigger_type,
77
+ twap_times=times,
78
+ twap_slippage_frac=slippage_frac,
79
+ )
80
+
81
+ # Create the base order
82
+ order_params = OrderParams(
83
+ sender=sender,
84
+ priceX18=int(price_x18),
85
+ amount=int(total_amount_x18),
86
+ expiration=expiration,
87
+ nonce=nonce,
88
+ appendix=appendix,
89
+ )
90
+
91
+ # Create trigger criteria
92
+ from nado_protocol.trigger_client.types.models import TimeTriggerData
93
+
94
+ trigger = TimeTrigger(
95
+ time_trigger=TimeTriggerData(
96
+ interval=interval_seconds,
97
+ amounts=custom_amounts_x18,
98
+ )
99
+ )
100
+
101
+ return PlaceTriggerOrderParams(
102
+ product_id=product_id,
103
+ order=order_params,
104
+ trigger=trigger,
105
+ signature=None, # Will be filled by client
106
+ digest=None, # Will be filled by client
107
+ spot_leverage=spot_leverage,
108
+ id=id,
109
+ )
110
+
111
+
112
+ def validate_twap_order(
113
+ total_amount_x18: str,
114
+ times: int,
115
+ custom_amounts_x18: Optional[List[str]] = None,
116
+ ) -> None:
117
+ """
118
+ Validate TWAP order parameters.
119
+
120
+ Args:
121
+ total_amount_x18 (str): The total amount to trade multiplied by 1e18.
122
+ times (int): Number of TWAP executions.
123
+ custom_amounts_x18 (Optional[List[str]]): Custom amounts for each execution multiplied by 1e18.
124
+
125
+ Raises:
126
+ ValueError: If validation fails.
127
+ """
128
+ total_amount_int = int(total_amount_x18)
129
+
130
+ if custom_amounts_x18 is None:
131
+ # For equal distribution, total amount must be divisible by times
132
+ if total_amount_int % times != 0:
133
+ raise ValueError(
134
+ f"Total amount {total_amount_x18} must be divisible by times {times} "
135
+ f"for equal distribution TWAP orders"
136
+ )
137
+ else:
138
+ # For custom amounts, verify the list length and sum
139
+ if len(custom_amounts_x18) != times:
140
+ raise ValueError(
141
+ f"Custom amounts list length ({len(custom_amounts_x18)}) must equal "
142
+ f"times ({times})"
143
+ )
144
+
145
+ custom_sum = sum(int(amount) for amount in custom_amounts_x18)
146
+ if custom_sum != total_amount_int:
147
+ raise ValueError(
148
+ f"Sum of custom amounts ({custom_sum}) must equal "
149
+ f"total amount ({total_amount_int})"
150
+ )
151
+
152
+
153
+ def estimate_twap_completion_time(times: int, interval_seconds: int) -> int:
154
+ """
155
+ Estimate the total time for TWAP order completion.
156
+
157
+ Args:
158
+ times (int): Number of TWAP executions.
159
+ interval_seconds (int): Time interval between executions.
160
+
161
+ Returns:
162
+ int: Estimated completion time in seconds.
163
+ """
164
+ return (times - 1) * interval_seconds
165
+
166
+
167
+ def calculate_equal_amounts(total_amount_x18: str, times: int) -> List[str]:
168
+ """
169
+ Calculate equal amounts for TWAP executions.
170
+
171
+ Args:
172
+ total_amount_x18 (str): The total amount to distribute multiplied by 1e18.
173
+ times (int): Number of executions.
174
+
175
+ Returns:
176
+ List[str]: List of equal amounts for each execution multiplied by 1e18.
177
+
178
+ Raises:
179
+ ValueError: If total amount is not divisible by times.
180
+ """
181
+ total_amount_int = int(total_amount_x18)
182
+
183
+ if total_amount_int % times != 0:
184
+ raise ValueError(
185
+ f"Total amount {total_amount_x18} is not divisible by times {times}"
186
+ )
187
+
188
+ amount_per_execution = total_amount_int // times
189
+ return [str(amount_per_execution)] * times
@@ -0,0 +1,309 @@
1
+ Metadata-Version: 2.4
2
+ Name: nado-protocol
3
+ Version: 0.1.6
4
+ Summary: Nado Protocol SDK
5
+ Keywords: nado protocol,nado sdk,nado protocol api
6
+ Author: Jeury Mejia
7
+ Author-email: jeury@inkfnd.com
8
+ Maintainer: Frank Jia
9
+ Maintainer-email: frank@inkfnd.com
10
+ Requires-Python: >=3.9,<4.0
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Requires-Dist: eth-account (>=0.8.0,<0.9.0)
19
+ Requires-Dist: pydantic (>=1.10.7,<2.0.0)
20
+ Requires-Dist: web3 (>=6.4.0,<7.0.0)
21
+ Project-URL: Documentation, https://nadohq.github.io/nado-python-sdk/
22
+ Project-URL: Homepage, https://nado.xyz
23
+ Description-Content-Type: text/markdown
24
+
25
+ # Nado Protocol Python SDK
26
+
27
+ This is the Python SDK for the [Nado Protocol API](TODO).
28
+
29
+ See [SDK docs](https://nadohq.github.io/nado-python-sdk/index.html) to get started.
30
+
31
+ ## Requirements
32
+
33
+ - Python 3.9 or above
34
+
35
+ ## Installation
36
+
37
+ You can install the SDK via pip:
38
+
39
+ ```bash
40
+ pip install nado-protocol
41
+ ```
42
+
43
+ ## Basic usage
44
+
45
+ ### Import the necessary utilities:
46
+
47
+ ```python
48
+ from nado_protocol.client import create_nado_client, NadoClientMode
49
+ from nado_protocol.contracts.types import DepositCollateralParams
50
+ from nado_protocol.engine_client.types.execute import (
51
+ OrderParams,
52
+ PlaceOrderParams,
53
+ SubaccountParams
54
+ )
55
+ from nado_protocol.utils.expiration import OrderType, get_expiration_timestamp
56
+ from nado_protocol.utils.math import to_pow_10, to_x18
57
+ from nado_protocol.utils.nonce import gen_order_nonce
58
+ from nado_protocol.utils.order import build_appendix
59
+ ```
60
+
61
+ ### Create the NadoClient providing your private key:
62
+
63
+ ```python
64
+ print("setting up nado client...")
65
+ private_key = "xxx"
66
+ client = create_nado_client(NadoClientMode.DEVNET, private_key)
67
+ ```
68
+
69
+ ### Perform basic operations:
70
+
71
+ ```python
72
+ # Depositing collaterals
73
+ print("approving allowance...")
74
+ approve_allowance_tx_hash = client.spot.approve_allowance(0, to_pow_10(100000, 6))
75
+ print("approve allowance tx hash:", approve_allowance_tx_hash)
76
+
77
+ print("querying my allowance...")
78
+ token_allowance = client.spot.get_token_allowance(0, client.context.signer.address)
79
+ print("token allowance:", token_allowance)
80
+
81
+ print("depositing collateral...")
82
+ deposit_tx_hash = client.spot.deposit(
83
+ DepositCollateralParams(
84
+ subaccount_name="default", product_id=0, amount=to_pow_10(100000, 6)
85
+ )
86
+ )
87
+ print("deposit collateral tx hash:", deposit_tx_hash)
88
+
89
+ # Placing orders
90
+ print("placing order...")
91
+ owner = client.context.engine_client.signer.address
92
+ product_id = 1
93
+ order = OrderParams(
94
+ sender=SubaccountParams(
95
+ subaccount_owner=owner,
96
+ subaccount_name="default",
97
+ ),
98
+ priceX18=to_x18(20000),
99
+ amount=to_pow_10(1, 17),
100
+ expiration=get_expiration_timestamp(40),
101
+ nonce=gen_order_nonce(),
102
+ appendix=build_appendix(order_type=OrderType.POST_ONLY)
103
+ )
104
+ res = client.market.place_order({"product_id": product_id, "order": order})
105
+ print("order result:", res.json(indent=2))
106
+ ```
107
+
108
+ ## TWAP and Trigger Orders
109
+
110
+ The SDK provides comprehensive support for Time-Weighted Average Price (TWAP) orders and price trigger orders through the Trigger Client.
111
+
112
+ ### TWAP Orders
113
+
114
+ TWAP orders allow you to execute large trades over time with controlled slippage:
115
+
116
+ ```python
117
+ from nado_protocol.trigger_client import TriggerClient
118
+ from nado_protocol.trigger_client.types import TriggerClientOpts
119
+ from nado_protocol.utils.math import to_x18
120
+ from nado_protocol.utils.expiration import get_expiration_timestamp
121
+
122
+ # Create trigger client
123
+ trigger_client = TriggerClient(
124
+ opts=TriggerClientOpts(url=TRIGGER_BACKEND_URL, signer=private_key)
125
+ )
126
+
127
+ # Place a TWAP order to buy 5 BTC over 2 hours
128
+ twap_result = trigger_client.place_twap_order(
129
+ product_id=1,
130
+ sender=client.signer.address,
131
+ price_x18=str(to_x18(50_000)), # Max $50k per execution
132
+ total_amount_x18=str(to_x18(5)), # Buy 5 BTC total
133
+ expiration=get_expiration_timestamp(60 * 24), # 24 hours
134
+ nonce=client.order_nonce(),
135
+ times=10, # Split into 10 executions
136
+ slippage_frac=0.005, # 0.5% slippage tolerance
137
+ interval_seconds=720, # 12 minutes between executions
138
+ )
139
+ ```
140
+
141
+ ### TWAP with Custom Amounts
142
+
143
+ For more sophisticated strategies, you can specify custom amounts for each execution:
144
+
145
+ ```python
146
+ # Decreasing size strategy: 2 BTC, 1.5 BTC, 1 BTC, 0.5 BTC
147
+ custom_amounts = [
148
+ str(to_x18(2)), # 2 BTC
149
+ str(to_x18(1.5)), # 1.5 BTC
150
+ str(to_x18(1)), # 1 BTC
151
+ str(to_x18(0.5)), # 0.5 BTC
152
+ ]
153
+
154
+ custom_twap_result = trigger_client.place_twap_order(
155
+ product_id=1,
156
+ sender=client.signer.address,
157
+ price_x18=str(to_x18(51_000)),
158
+ total_amount_x18=str(to_x18(5)), # 5 BTC total
159
+ expiration=get_expiration_timestamp(60 * 12),
160
+ nonce=client.order_nonce(),
161
+ times=4, # 4 executions
162
+ slippage_frac=0.01, # 1% slippage
163
+ interval_seconds=1800, # 30 minutes
164
+ custom_amounts_x18=custom_amounts,
165
+ )
166
+ ```
167
+
168
+ ### Price Trigger Orders
169
+
170
+ Create conditional orders that execute when price conditions are met:
171
+
172
+ ```python
173
+ # Stop-loss order (sell when price drops below $45k)
174
+ stop_loss = trigger_client.place_price_trigger_order(
175
+ product_id=1,
176
+ sender=client.signer.address,
177
+ price_x18=str(to_x18(44_000)), # Sell at $44k
178
+ amount_x18=str(-to_x18(1)), # Sell 1 BTC (negative for sell)
179
+ expiration=get_expiration_timestamp(60 * 24 * 7), # 1 week
180
+ nonce=client.order_nonce(),
181
+ trigger_price_x18=str(to_x18(45_000)), # Trigger below $45k
182
+ trigger_type="last_price_below",
183
+ reduce_only=True, # Only reduce position
184
+ )
185
+
186
+ # Take-profit order (sell when price rises above $55k)
187
+ take_profit = trigger_client.place_price_trigger_order(
188
+ product_id=1,
189
+ sender=client.signer.address,
190
+ price_x18=str(to_x18(56_000)), # Sell at $56k
191
+ amount_x18=str(-to_x18(1)), # Sell 1 BTC
192
+ expiration=get_expiration_timestamp(60 * 24 * 7),
193
+ nonce=client.order_nonce(),
194
+ trigger_price_x18=str(to_x18(55_000)), # Trigger above $55k
195
+ trigger_type="last_price_above",
196
+ reduce_only=True,
197
+ )
198
+ ```
199
+
200
+ ### Supported Trigger Types
201
+
202
+ The SDK supports six types of price triggers:
203
+
204
+ - `"last_price_above"`: Trigger when last traded price goes above threshold
205
+ - `"last_price_below"`: Trigger when last traded price goes below threshold
206
+ - `"oracle_price_above"`: Trigger when oracle price goes above threshold
207
+ - `"oracle_price_below"`: Trigger when oracle price goes below threshold
208
+ - `"mid_price_above"`: Trigger when mid price goes above threshold
209
+ - `"mid_price_below"`: Trigger when mid price goes below threshold
210
+
211
+ ### Complete Trading Strategy Example
212
+
213
+ Here's how to set up a complete trading strategy with stop-loss, take-profit, and DCA:
214
+
215
+ ```python
216
+ # 1. Stop-loss protection
217
+ stop_loss = trigger_client.place_price_trigger_order(
218
+ product_id=1,
219
+ sender=client.signer.address,
220
+ price_x18=str(to_x18(44_000)),
221
+ amount_x18=str(-to_x18(2)), # Close 2 BTC position
222
+ expiration=get_expiration_timestamp(60 * 24 * 30),
223
+ nonce=client.order_nonce(),
224
+ trigger_price_x18=str(to_x18(45_000)),
225
+ trigger_type="last_price_below",
226
+ reduce_only=True,
227
+ )
228
+
229
+ # 2. Take-profit target
230
+ take_profit = trigger_client.place_price_trigger_order(
231
+ product_id=1,
232
+ sender=client.signer.address,
233
+ price_x18=str(to_x18(58_000)),
234
+ amount_x18=str(-to_x18(2)), # Close 2 BTC position
235
+ expiration=get_expiration_timestamp(60 * 24 * 30),
236
+ nonce=client.order_nonce(),
237
+ trigger_price_x18=str(to_x18(57_000)),
238
+ trigger_type="last_price_above",
239
+ reduce_only=True,
240
+ )
241
+
242
+ # 3. DCA accumulation strategy
243
+ dca_strategy = trigger_client.place_twap_order(
244
+ product_id=1,
245
+ sender=client.signer.address,
246
+ price_x18=str(to_x18(52_000)), # Max $52k per buy
247
+ total_amount_x18=str(to_x18(10)), # Buy 10 BTC over time
248
+ expiration=get_expiration_timestamp(60 * 24 * 7),
249
+ nonce=client.order_nonce(),
250
+ times=20, # 20 executions
251
+ slippage_frac=0.005, # 0.5% slippage
252
+ interval_seconds=1800, # 30 minutes
253
+ )
254
+ ```
255
+
256
+ See [Getting Started](https://nadohq.github.io/nado-python-sdk/getting-started.html) for more.
257
+
258
+ ## Running locally
259
+
260
+ 1. Clone [github repo](https://github.com/nadohq/nado-python-sdk)
261
+
262
+ 2. Install poetry
263
+
264
+ ```
265
+
266
+ $ curl -sSL https://install.python-poetry.org | python3 -
267
+
268
+ ```
269
+
270
+ 3. Setup a virtual environment and activate it
271
+
272
+ ```
273
+
274
+ $ python3 -m venv venv
275
+ $ source ./venv/bin/activate
276
+
277
+ ```
278
+
279
+ 4. Install dependencies via `poetry install`
280
+ 5. Setup an `.env` file and set the following envvars
281
+
282
+ ```shell
283
+ CLIENT_MODE='devnet'
284
+ SIGNER_PRIVATE_KEY="0x..."
285
+ LINKED_SIGNER_PRIVATE_KEY="0x..." # not required
286
+ ```
287
+
288
+ ### Run tests
289
+
290
+ ```
291
+ $ poetry run test
292
+ ```
293
+
294
+ ### Run sanity checks
295
+
296
+ - `poetry run client-sanity`: runs sanity checks for the top-level client.
297
+ - `poetry run engine-sanity`: runs sanity checks for the `engine-client`.
298
+ - `poetry run indexer-sanity`: runs sanity checks for the `indexer-client`.
299
+ - `poetry run trigger-sanity`: runs sanity checks for the `trigger-client` including TWAP and price trigger examples.
300
+ - `poetry run contracts-sanity`: runs sanity checks for the contracts module.
301
+
302
+ ### Build Docs
303
+
304
+ To build the docs locally run:
305
+
306
+ ```
307
+ $ poetry run sphinx-build docs/source docs/build
308
+ ```
309
+