crypticorn 2.16.0__py3-none-any.whl → 2.17.0__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.
Files changed (164) hide show
  1. crypticorn/__init__.py +2 -2
  2. crypticorn/auth/client/api/admin_api.py +397 -13
  3. crypticorn/auth/client/api/auth_api.py +3610 -341
  4. crypticorn/auth/client/api/service_api.py +249 -7
  5. crypticorn/auth/client/api/user_api.py +2295 -179
  6. crypticorn/auth/client/api/wallet_api.py +1468 -81
  7. crypticorn/auth/client/configuration.py +2 -2
  8. crypticorn/auth/client/models/create_api_key_request.py +2 -1
  9. crypticorn/auth/client/models/get_api_keys200_response_inner.py +2 -1
  10. crypticorn/auth/client/rest.py +23 -4
  11. crypticorn/auth/main.py +8 -5
  12. crypticorn/cli/init.py +1 -1
  13. crypticorn/cli/templates/.env.docker.temp +3 -0
  14. crypticorn/cli/templates/.env.example.temp +4 -0
  15. crypticorn/cli/templates/Dockerfile +5 -2
  16. crypticorn/client.py +226 -59
  17. crypticorn/common/__init__.py +1 -0
  18. crypticorn/common/auth.py +45 -14
  19. crypticorn/common/decorators.py +1 -2
  20. crypticorn/common/enums.py +0 -2
  21. crypticorn/common/errors.py +10 -0
  22. crypticorn/common/metrics.py +30 -0
  23. crypticorn/common/middleware.py +94 -1
  24. crypticorn/common/pagination.py +252 -20
  25. crypticorn/common/router/admin_router.py +2 -2
  26. crypticorn/common/router/status_router.py +40 -2
  27. crypticorn/common/scopes.py +2 -2
  28. crypticorn/common/warnings.py +7 -0
  29. crypticorn/dex/__init__.py +6 -0
  30. crypticorn/dex/client/__init__.py +49 -0
  31. crypticorn/dex/client/api/__init__.py +6 -0
  32. crypticorn/dex/client/api/admin_api.py +2986 -0
  33. crypticorn/dex/client/api/signals_api.py +1798 -0
  34. crypticorn/dex/client/api/status_api.py +892 -0
  35. crypticorn/dex/client/api_client.py +758 -0
  36. crypticorn/dex/client/api_response.py +20 -0
  37. crypticorn/dex/client/configuration.py +620 -0
  38. crypticorn/dex/client/exceptions.py +220 -0
  39. crypticorn/dex/client/models/__init__.py +30 -0
  40. crypticorn/dex/client/models/api_error_identifier.py +121 -0
  41. crypticorn/dex/client/models/api_error_level.py +37 -0
  42. crypticorn/dex/client/models/api_error_type.py +37 -0
  43. crypticorn/dex/client/models/exception_detail.py +117 -0
  44. crypticorn/dex/client/models/log_level.py +38 -0
  45. crypticorn/dex/client/models/paginated_response_signal_with_token.py +134 -0
  46. crypticorn/dex/client/models/risk.py +86 -0
  47. crypticorn/dex/client/models/signal_overview_stats.py +158 -0
  48. crypticorn/dex/client/models/signal_volume.py +84 -0
  49. crypticorn/dex/client/models/signal_with_token.py +163 -0
  50. crypticorn/dex/client/models/token_data.py +127 -0
  51. crypticorn/dex/client/models/token_detail.py +116 -0
  52. crypticorn/dex/client/py.typed +0 -0
  53. crypticorn/dex/client/rest.py +217 -0
  54. crypticorn/dex/main.py +1 -0
  55. crypticorn/hive/client/api/admin_api.py +1173 -47
  56. crypticorn/hive/client/api/data_api.py +499 -17
  57. crypticorn/hive/client/api/models_api.py +1595 -87
  58. crypticorn/hive/client/api/status_api.py +397 -16
  59. crypticorn/hive/client/api_client.py +0 -5
  60. crypticorn/hive/client/models/api_error_identifier.py +1 -1
  61. crypticorn/hive/client/models/coin_info.py +1 -1
  62. crypticorn/hive/client/models/exception_detail.py +1 -1
  63. crypticorn/hive/client/models/target_info.py +1 -1
  64. crypticorn/hive/client/rest.py +23 -4
  65. crypticorn/hive/main.py +99 -25
  66. crypticorn/hive/utils.py +2 -2
  67. crypticorn/klines/client/api/admin_api.py +1173 -47
  68. crypticorn/klines/client/api/change_in_timeframe_api.py +269 -11
  69. crypticorn/klines/client/api/funding_rates_api.py +315 -11
  70. crypticorn/klines/client/api/ohlcv_data_api.py +390 -11
  71. crypticorn/klines/client/api/status_api.py +397 -16
  72. crypticorn/klines/client/api/symbols_api.py +216 -11
  73. crypticorn/klines/client/api/udf_api.py +1268 -51
  74. crypticorn/klines/client/api_client.py +0 -5
  75. crypticorn/klines/client/models/api_error_identifier.py +3 -1
  76. crypticorn/klines/client/models/exception_detail.py +1 -1
  77. crypticorn/klines/client/models/ohlcv.py +1 -1
  78. crypticorn/klines/client/models/symbol_group.py +1 -1
  79. crypticorn/klines/client/models/udf_config.py +1 -1
  80. crypticorn/klines/client/rest.py +23 -4
  81. crypticorn/klines/main.py +89 -12
  82. crypticorn/metrics/client/api/admin_api.py +1173 -47
  83. crypticorn/metrics/client/api/exchanges_api.py +1370 -145
  84. crypticorn/metrics/client/api/indicators_api.py +622 -17
  85. crypticorn/metrics/client/api/logs_api.py +296 -11
  86. crypticorn/metrics/client/api/marketcap_api.py +1207 -67
  87. crypticorn/metrics/client/api/markets_api.py +343 -11
  88. crypticorn/metrics/client/api/quote_currencies_api.py +228 -11
  89. crypticorn/metrics/client/api/status_api.py +397 -16
  90. crypticorn/metrics/client/api/tokens_api.py +382 -15
  91. crypticorn/metrics/client/api_client.py +0 -5
  92. crypticorn/metrics/client/configuration.py +4 -2
  93. crypticorn/metrics/client/models/exception_detail.py +1 -1
  94. crypticorn/metrics/client/models/exchange_mapping.py +1 -1
  95. crypticorn/metrics/client/models/marketcap_ranking.py +1 -1
  96. crypticorn/metrics/client/models/marketcap_symbol_ranking.py +1 -1
  97. crypticorn/metrics/client/models/ohlcv.py +1 -1
  98. crypticorn/metrics/client/rest.py +23 -4
  99. crypticorn/metrics/main.py +113 -19
  100. crypticorn/pay/client/api/admin_api.py +1585 -57
  101. crypticorn/pay/client/api/now_payments_api.py +961 -39
  102. crypticorn/pay/client/api/payments_api.py +562 -17
  103. crypticorn/pay/client/api/products_api.py +880 -30
  104. crypticorn/pay/client/api/status_api.py +397 -16
  105. crypticorn/pay/client/api_client.py +0 -5
  106. crypticorn/pay/client/configuration.py +2 -2
  107. crypticorn/pay/client/models/api_error_identifier.py +7 -7
  108. crypticorn/pay/client/models/exception_detail.py +1 -1
  109. crypticorn/pay/client/models/now_create_invoice_req.py +1 -1
  110. crypticorn/pay/client/models/now_create_invoice_res.py +1 -1
  111. crypticorn/pay/client/models/product.py +1 -1
  112. crypticorn/pay/client/models/product_create.py +1 -1
  113. crypticorn/pay/client/models/product_update.py +1 -1
  114. crypticorn/pay/client/models/scope.py +1 -0
  115. crypticorn/pay/client/rest.py +23 -4
  116. crypticorn/pay/main.py +10 -6
  117. crypticorn/trade/client/__init__.py +11 -1
  118. crypticorn/trade/client/api/__init__.py +0 -1
  119. crypticorn/trade/client/api/admin_api.py +1184 -55
  120. crypticorn/trade/client/api/api_keys_api.py +1678 -162
  121. crypticorn/trade/client/api/bots_api.py +7563 -187
  122. crypticorn/trade/client/api/exchanges_api.py +565 -19
  123. crypticorn/trade/client/api/notifications_api.py +1290 -116
  124. crypticorn/trade/client/api/orders_api.py +393 -55
  125. crypticorn/trade/client/api/status_api.py +397 -13
  126. crypticorn/trade/client/api/strategies_api.py +1133 -77
  127. crypticorn/trade/client/api/trading_actions_api.py +786 -65
  128. crypticorn/trade/client/models/__init__.py +11 -0
  129. crypticorn/trade/client/models/actions_count.py +88 -0
  130. crypticorn/trade/client/models/api_error_identifier.py +1 -0
  131. crypticorn/trade/client/models/bot.py +7 -18
  132. crypticorn/trade/client/models/bot_create.py +17 -1
  133. crypticorn/trade/client/models/bot_update.py +17 -1
  134. crypticorn/trade/client/models/exchange.py +6 -1
  135. crypticorn/trade/client/models/exchange_key.py +1 -1
  136. crypticorn/trade/client/models/exchange_key_balance.py +111 -0
  137. crypticorn/trade/client/models/exchange_key_create.py +17 -1
  138. crypticorn/trade/client/models/exchange_key_update.py +17 -1
  139. crypticorn/trade/client/models/execution_ids.py +1 -1
  140. crypticorn/trade/client/models/futures_balance.py +27 -25
  141. crypticorn/trade/client/models/notification.py +17 -1
  142. crypticorn/trade/client/models/notification_create.py +18 -2
  143. crypticorn/trade/client/models/notification_update.py +17 -1
  144. crypticorn/trade/client/models/orders_count.py +88 -0
  145. crypticorn/trade/client/models/paginated_response_futures_trading_action.py +134 -0
  146. crypticorn/trade/client/models/paginated_response_order.py +134 -0
  147. crypticorn/trade/client/models/pn_l.py +95 -0
  148. crypticorn/trade/client/models/post_futures_action.py +1 -1
  149. crypticorn/trade/client/models/spot_balance.py +109 -0
  150. crypticorn/trade/client/models/strategy.py +22 -4
  151. crypticorn/trade/client/models/strategy_create.py +23 -5
  152. crypticorn/trade/client/models/strategy_exchange_info.py +16 -4
  153. crypticorn/trade/client/models/strategy_update.py +19 -3
  154. crypticorn/trade/client/models/tpsl.py +4 -19
  155. crypticorn/trade/client/models/tpsl_create.py +6 -19
  156. crypticorn/trade/client/rest.py +23 -4
  157. crypticorn/trade/main.py +15 -12
  158. {crypticorn-2.16.0.dist-info → crypticorn-2.17.0.dist-info}/METADATA +65 -20
  159. {crypticorn-2.16.0.dist-info → crypticorn-2.17.0.dist-info}/RECORD +163 -128
  160. crypticorn/trade/client/api/futures_trading_panel_api.py +0 -1285
  161. {crypticorn-2.16.0.dist-info → crypticorn-2.17.0.dist-info}/WHEEL +0 -0
  162. {crypticorn-2.16.0.dist-info → crypticorn-2.17.0.dist-info}/entry_points.txt +0 -0
  163. {crypticorn-2.16.0.dist-info → crypticorn-2.17.0.dist-info}/licenses/LICENSE +0 -0
  164. {crypticorn-2.16.0.dist-info → crypticorn-2.17.0.dist-info}/top_level.txt +0 -0
@@ -50,15 +50,18 @@ class Strategy(BaseModel):
50
50
  description="Whether the strategy is enabled. If false, no bots will be created or updated for this strategy, open trades will be rejected. Existing bots will be marked as stopping."
51
51
  )
52
52
  performance_fee: Union[
53
- Annotated[float, Field(le=1.0, strict=True)],
54
- Annotated[int, Field(le=1, strict=True)],
53
+ Annotated[float, Field(le=1.0, strict=True, ge=0.0)],
54
+ Annotated[int, Field(le=1, strict=True, ge=0)],
55
55
  ] = Field(description="Performance fee for the strategy")
56
56
  identifier: StrictStr = Field(
57
57
  description="Unique human readable identifier for the strategy e.g. 'daily_trend_momentum'"
58
58
  )
59
59
  margin_mode: Optional[MarginMode] = None
60
- leverage: StrictInt = Field(description="Leverage for the strategy")
60
+ leverage: Annotated[int, Field(strict=True, ge=1)] = Field(
61
+ description="Leverage for the strategy"
62
+ )
61
63
  market_type: MarketType = Field(description="Market of operation of the strategy")
64
+ additional_properties: Dict[str, Any] = {}
62
65
  __properties: ClassVar[List[str]] = [
63
66
  "created_at",
64
67
  "updated_at",
@@ -103,8 +106,13 @@ class Strategy(BaseModel):
103
106
  * `None` is only added to the output dict for nullable fields that
104
107
  were set at model initialization. Other fields with value `None`
105
108
  are ignored.
109
+ * Fields in `self.additional_properties` are added to the output dict.
106
110
  """
107
- excluded_fields: Set[str] = set([])
111
+ excluded_fields: Set[str] = set(
112
+ [
113
+ "additional_properties",
114
+ ]
115
+ )
108
116
 
109
117
  _dict = self.model_dump(
110
118
  by_alias=True,
@@ -118,6 +126,11 @@ class Strategy(BaseModel):
118
126
  if _item_exchanges:
119
127
  _items.append(_item_exchanges.to_dict())
120
128
  _dict["exchanges"] = _items
129
+ # puts key-value pairs in additional_properties in the top level
130
+ if self.additional_properties is not None:
131
+ for _key, _value in self.additional_properties.items():
132
+ _dict[_key] = _value
133
+
121
134
  # set to None if margin_mode (nullable) is None
122
135
  # and model_fields_set contains the field
123
136
  if self.margin_mode is None and "margin_mode" in self.model_fields_set:
@@ -157,4 +170,9 @@ class Strategy(BaseModel):
157
170
  "market_type": obj.get("market_type"),
158
171
  }
159
172
  )
173
+ # store additional fields in additional_properties
174
+ for _key in obj.keys():
175
+ if _key not in cls.__properties:
176
+ _obj.additional_properties[_key] = obj.get(_key)
177
+
160
178
  return _obj
@@ -17,7 +17,7 @@ import pprint
17
17
  import re # noqa: F401
18
18
  import json
19
19
 
20
- from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictInt, StrictStr
20
+ from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictStr
21
21
  from typing import Any, ClassVar, Dict, List, Optional, Union
22
22
  from typing_extensions import Annotated
23
23
  from crypticorn.trade.client.models.margin_mode import MarginMode
@@ -41,15 +41,18 @@ class StrategyCreate(BaseModel):
41
41
  description="Whether the strategy is enabled. If false, no bots will be created or updated for this strategy, open trades will be rejected. Existing bots will be marked as stopping."
42
42
  )
43
43
  performance_fee: Union[
44
- Annotated[float, Field(le=1.0, strict=True)],
45
- Annotated[int, Field(le=1, strict=True)],
44
+ Annotated[float, Field(le=1.0, strict=True, ge=0.0)],
45
+ Annotated[int, Field(le=1, strict=True, ge=0)],
46
46
  ] = Field(description="Performance fee for the strategy")
47
47
  identifier: StrictStr = Field(
48
48
  description="Unique human readable identifier for the strategy e.g. 'daily_trend_momentum'"
49
49
  )
50
50
  margin_mode: Optional[MarginMode] = None
51
- leverage: StrictInt = Field(description="Leverage for the strategy")
51
+ leverage: Annotated[int, Field(strict=True, ge=1)] = Field(
52
+ description="Leverage for the strategy"
53
+ )
52
54
  market_type: MarketType = Field(description="Market of operation of the strategy")
55
+ additional_properties: Dict[str, Any] = {}
53
56
  __properties: ClassVar[List[str]] = [
54
57
  "name",
55
58
  "description",
@@ -91,8 +94,13 @@ class StrategyCreate(BaseModel):
91
94
  * `None` is only added to the output dict for nullable fields that
92
95
  were set at model initialization. Other fields with value `None`
93
96
  are ignored.
97
+ * Fields in `self.additional_properties` are added to the output dict.
94
98
  """
95
- excluded_fields: Set[str] = set([])
99
+ excluded_fields: Set[str] = set(
100
+ [
101
+ "additional_properties",
102
+ ]
103
+ )
96
104
 
97
105
  _dict = self.model_dump(
98
106
  by_alias=True,
@@ -106,6 +114,11 @@ class StrategyCreate(BaseModel):
106
114
  if _item_exchanges:
107
115
  _items.append(_item_exchanges.to_dict())
108
116
  _dict["exchanges"] = _items
117
+ # puts key-value pairs in additional_properties in the top level
118
+ if self.additional_properties is not None:
119
+ for _key, _value in self.additional_properties.items():
120
+ _dict[_key] = _value
121
+
109
122
  # set to None if margin_mode (nullable) is None
110
123
  # and model_fields_set contains the field
111
124
  if self.margin_mode is None and "margin_mode" in self.model_fields_set:
@@ -142,4 +155,9 @@ class StrategyCreate(BaseModel):
142
155
  "market_type": obj.get("market_type"),
143
156
  }
144
157
  )
158
+ # store additional fields in additional_properties
159
+ for _key in obj.keys():
160
+ if _key not in cls.__properties:
161
+ _obj.additional_properties[_key] = obj.get(_key)
162
+
145
163
  return _obj
@@ -17,8 +17,8 @@ import pprint
17
17
  import re # noqa: F401
18
18
  import json
19
19
 
20
- from pydantic import BaseModel, ConfigDict, Field, StrictInt
21
- from typing import Any, ClassVar, Dict, List
20
+ from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr
21
+ from typing import Any, ClassVar, Dict, List, Optional
22
22
  from crypticorn.trade.client.models.exchange import Exchange
23
23
  from typing import Optional, Set
24
24
  from typing_extensions import Self
@@ -30,10 +30,14 @@ class StrategyExchangeInfo(BaseModel):
30
30
  """ # noqa: E501
31
31
 
32
32
  exchange: Exchange = Field(description="Exchange name. Of type Exchange")
33
+ base_asset: Optional[StrictStr] = Field(
34
+ default="USDT",
35
+ description="Base asset for the strategy. This is the asset that will be used to trade with. Default is USDT.",
36
+ )
33
37
  min_amount: StrictInt = Field(
34
38
  description="Minimum amount for the strategy on the exchange"
35
39
  )
36
- __properties: ClassVar[List[str]] = ["exchange", "min_amount"]
40
+ __properties: ClassVar[List[str]] = ["exchange", "base_asset", "min_amount"]
37
41
 
38
42
  model_config = ConfigDict(
39
43
  populate_by_name=True,
@@ -84,6 +88,14 @@ class StrategyExchangeInfo(BaseModel):
84
88
  return cls.model_validate(obj)
85
89
 
86
90
  _obj = cls.model_validate(
87
- {"exchange": obj.get("exchange"), "min_amount": obj.get("min_amount")}
91
+ {
92
+ "exchange": obj.get("exchange"),
93
+ "base_asset": (
94
+ obj.get("base_asset")
95
+ if obj.get("base_asset") is not None
96
+ else "USDT"
97
+ ),
98
+ "min_amount": obj.get("min_amount"),
99
+ }
88
100
  )
89
101
  return _obj
@@ -36,10 +36,11 @@ class StrategyUpdate(BaseModel):
36
36
  enabled: Optional[StrictBool] = None
37
37
  performance_fee: Optional[
38
38
  Union[
39
- Annotated[float, Field(le=1.0, strict=True)],
40
- Annotated[int, Field(le=1, strict=True)],
39
+ Annotated[float, Field(le=1.0, strict=True, ge=0.0)],
40
+ Annotated[int, Field(le=1, strict=True, ge=0)],
41
41
  ]
42
42
  ] = None
43
+ additional_properties: Dict[str, Any] = {}
43
44
  __properties: ClassVar[List[str]] = [
44
45
  "name",
45
46
  "description",
@@ -77,8 +78,13 @@ class StrategyUpdate(BaseModel):
77
78
  * `None` is only added to the output dict for nullable fields that
78
79
  were set at model initialization. Other fields with value `None`
79
80
  are ignored.
81
+ * Fields in `self.additional_properties` are added to the output dict.
80
82
  """
81
- excluded_fields: Set[str] = set([])
83
+ excluded_fields: Set[str] = set(
84
+ [
85
+ "additional_properties",
86
+ ]
87
+ )
82
88
 
83
89
  _dict = self.model_dump(
84
90
  by_alias=True,
@@ -92,6 +98,11 @@ class StrategyUpdate(BaseModel):
92
98
  if _item_exchanges:
93
99
  _items.append(_item_exchanges.to_dict())
94
100
  _dict["exchanges"] = _items
101
+ # puts key-value pairs in additional_properties in the top level
102
+ if self.additional_properties is not None:
103
+ for _key, _value in self.additional_properties.items():
104
+ _dict[_key] = _value
105
+
95
106
  # set to None if name (nullable) is None
96
107
  # and model_fields_set contains the field
97
108
  if self.name is None and "name" in self.model_fields_set:
@@ -144,4 +155,9 @@ class StrategyUpdate(BaseModel):
144
155
  "performance_fee": obj.get("performance_fee"),
145
156
  }
146
157
  )
158
+ # store additional fields in additional_properties
159
+ for _key in obj.keys():
160
+ if _key not in cls.__properties:
161
+ _obj.additional_properties[_key] = obj.get(_key)
162
+
147
163
  return _obj
@@ -28,18 +28,14 @@ class TPSL(BaseModel):
28
28
  Runtime fields for take profit and stop loss
29
29
  """ # noqa: E501
30
30
 
31
- price_delta: Optional[StrictStr] = None
32
- price: Optional[StrictStr] = None
31
+ price_delta: StrictStr = Field(
32
+ description="The price delta to calculate the limit price from the current market price, e.g. 1.01 for a TP of 1% on long"
33
+ )
33
34
  allocation: StrictStr = Field(
34
35
  description="Percentage of the open order to sell. All allocations must sum up to 1. Use this allocation again when closing the order."
35
36
  )
36
37
  execution_id: Optional[StrictStr] = None
37
- __properties: ClassVar[List[str]] = [
38
- "price_delta",
39
- "price",
40
- "allocation",
41
- "execution_id",
42
- ]
38
+ __properties: ClassVar[List[str]] = ["price_delta", "allocation", "execution_id"]
43
39
 
44
40
  model_config = ConfigDict(
45
41
  populate_by_name=True,
@@ -78,16 +74,6 @@ class TPSL(BaseModel):
78
74
  exclude=excluded_fields,
79
75
  exclude_none=True,
80
76
  )
81
- # set to None if price_delta (nullable) is None
82
- # and model_fields_set contains the field
83
- if self.price_delta is None and "price_delta" in self.model_fields_set:
84
- _dict["price_delta"] = None
85
-
86
- # set to None if price (nullable) is None
87
- # and model_fields_set contains the field
88
- if self.price is None and "price" in self.model_fields_set:
89
- _dict["price"] = None
90
-
91
77
  # set to None if execution_id (nullable) is None
92
78
  # and model_fields_set contains the field
93
79
  if self.execution_id is None and "execution_id" in self.model_fields_set:
@@ -107,7 +93,6 @@ class TPSL(BaseModel):
107
93
  _obj = cls.model_validate(
108
94
  {
109
95
  "price_delta": obj.get("price_delta"),
110
- "price": obj.get("price"),
111
96
  "allocation": obj.get("allocation"),
112
97
  "execution_id": obj.get("execution_id"),
113
98
  }
@@ -18,7 +18,7 @@ import re # noqa: F401
18
18
  import json
19
19
 
20
20
  from pydantic import BaseModel, ConfigDict, Field, StrictStr
21
- from typing import Any, ClassVar, Dict, List, Optional
21
+ from typing import Any, ClassVar, Dict, List
22
22
  from typing import Optional, Set
23
23
  from typing_extensions import Self
24
24
 
@@ -28,12 +28,13 @@ class TPSLCreate(BaseModel):
28
28
  Model for take profit and stop loss
29
29
  """ # noqa: E501
30
30
 
31
- price_delta: Optional[StrictStr] = None
32
- price: Optional[StrictStr] = None
31
+ price_delta: StrictStr = Field(
32
+ description="The price delta to calculate the limit price from the current market price, e.g. 1.01 for a TP of 1% on long"
33
+ )
33
34
  allocation: StrictStr = Field(
34
35
  description="Percentage of the open order to sell. All allocations must sum up to 1. Use this allocation again when closing the order."
35
36
  )
36
- __properties: ClassVar[List[str]] = ["price_delta", "price", "allocation"]
37
+ __properties: ClassVar[List[str]] = ["price_delta", "allocation"]
37
38
 
38
39
  model_config = ConfigDict(
39
40
  populate_by_name=True,
@@ -72,16 +73,6 @@ class TPSLCreate(BaseModel):
72
73
  exclude=excluded_fields,
73
74
  exclude_none=True,
74
75
  )
75
- # set to None if price_delta (nullable) is None
76
- # and model_fields_set contains the field
77
- if self.price_delta is None and "price_delta" in self.model_fields_set:
78
- _dict["price_delta"] = None
79
-
80
- # set to None if price (nullable) is None
81
- # and model_fields_set contains the field
82
- if self.price is None and "price" in self.model_fields_set:
83
- _dict["price"] = None
84
-
85
76
  return _dict
86
77
 
87
78
  @classmethod
@@ -94,10 +85,6 @@ class TPSLCreate(BaseModel):
94
85
  return cls.model_validate(obj)
95
86
 
96
87
  _obj = cls.model_validate(
97
- {
98
- "price_delta": obj.get("price_delta"),
99
- "price": obj.get("price"),
100
- "allocation": obj.get("allocation"),
101
- }
88
+ {"price_delta": obj.get("price_delta"), "allocation": obj.get("allocation")}
102
89
  )
103
90
  return _obj
@@ -77,6 +77,7 @@ class RESTClientObject:
77
77
 
78
78
  self.pool_manager: Optional[aiohttp.ClientSession] = None
79
79
  self.retry_client: Optional[aiohttp_retry.RetryClient] = None
80
+ self.is_sync: bool = False # Track whether this is sync or async mode
80
81
 
81
82
  async def close(self) -> None:
82
83
  if self.pool_manager:
@@ -170,7 +171,9 @@ class RESTClientObject:
170
171
 
171
172
  pool_manager: Union[aiohttp.ClientSession, aiohttp_retry.RetryClient]
172
173
 
173
- # https pool manager
174
+ # For sync operations, always use a fresh session
175
+ should_close_session = False
176
+
174
177
  if self.pool_manager is None:
175
178
  self.pool_manager = aiohttp.ClientSession(
176
179
  connector=aiohttp.TCPConnector(
@@ -178,6 +181,9 @@ class RESTClientObject:
178
181
  ),
179
182
  trust_env=True,
180
183
  )
184
+ # Only close session automatically in sync mode
185
+ should_close_session = self.is_sync
186
+
181
187
  pool_manager = self.pool_manager
182
188
 
183
189
  if self.retries is not None and method in ALLOW_RETRY_METHODS:
@@ -193,6 +199,19 @@ class RESTClientObject:
193
199
  )
194
200
  pool_manager = self.retry_client
195
201
 
196
- r = await pool_manager.request(**args)
197
-
198
- return RESTResponse(r)
202
+ try:
203
+ r = await pool_manager.request(**args)
204
+ # For sessions we're about to close, read the data immediately
205
+ if should_close_session:
206
+ response = RESTResponse(r)
207
+ await response.read() # Read data before closing session
208
+ return response
209
+ else:
210
+ return RESTResponse(r)
211
+ finally:
212
+ if should_close_session:
213
+ if self.retry_client is not None:
214
+ await self.retry_client.close()
215
+ self.retry_client = None
216
+ await self.pool_manager.close()
217
+ self.pool_manager = None
crypticorn/trade/main.py CHANGED
@@ -1,12 +1,11 @@
1
1
  from __future__ import annotations
2
2
  from typing import TYPE_CHECKING, Optional
3
- from crypticorn.trade import (
3
+ from crypticorn.trade.client import (
4
4
  ApiClient,
5
5
  APIKeysApi,
6
6
  BotsApi,
7
7
  Configuration,
8
8
  ExchangesApi,
9
- FuturesTradingPanelApi,
10
9
  NotificationsApi,
11
10
  OrdersApi,
12
11
  StatusApi,
@@ -26,19 +25,23 @@ class TradeClient:
26
25
  config_class = Configuration
27
26
 
28
27
  def __init__(
29
- self, config: Configuration, http_client: Optional[ClientSession] = None
28
+ self,
29
+ config: Configuration,
30
+ http_client: Optional[ClientSession] = None,
31
+ is_sync: bool = False,
30
32
  ):
31
33
  self.config = config
32
34
  self.base_client = ApiClient(configuration=self.config)
33
35
  if http_client is not None:
34
36
  self.base_client.rest_client.pool_manager = http_client
37
+ # Pass sync context to REST client for proper session management
38
+ self.base_client.rest_client.is_sync = is_sync
35
39
  # Instantiate all the endpoint clients
36
- self.bots = BotsApi(self.base_client)
37
- self.exchanges = ExchangesApi(self.base_client)
38
- self.notifications = NotificationsApi(self.base_client)
39
- self.orders = OrdersApi(self.base_client)
40
- self.status = StatusApi(self.base_client)
41
- self.strategies = StrategiesApi(self.base_client)
42
- self.actions = TradingActionsApi(self.base_client)
43
- self.futures = FuturesTradingPanelApi(self.base_client)
44
- self.keys = APIKeysApi(self.base_client)
40
+ self.bots = BotsApi(self.base_client, is_sync=is_sync)
41
+ self.exchanges = ExchangesApi(self.base_client, is_sync=is_sync)
42
+ self.notifications = NotificationsApi(self.base_client, is_sync=is_sync)
43
+ self.orders = OrdersApi(self.base_client, is_sync=is_sync)
44
+ self.status = StatusApi(self.base_client, is_sync=is_sync)
45
+ self.strategies = StrategiesApi(self.base_client, is_sync=is_sync)
46
+ self.actions = TradingActionsApi(self.base_client, is_sync=is_sync)
47
+ self.keys = APIKeysApi(self.base_client, is_sync=is_sync)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: crypticorn
3
- Version: 2.16.0
3
+ Version: 2.17.0
4
4
  Summary: Maximise Your Crypto Trading Profits with Machine Learning
5
5
  Author-email: Crypticorn <timon@crypticorn.com>
6
6
  License-Expression: MIT
@@ -25,6 +25,8 @@ Requires-Dist: click<9.0.0,>=8.0.0
25
25
  Requires-Dist: psutil<8.0.0,>=7.0.0
26
26
  Requires-Dist: setuptools<81.0.0,>=80.0.0
27
27
  Requires-Dist: strenum
28
+ Requires-Dist: prometheus-client<1.0.0,>=0.22.0
29
+ Requires-Dist: asgiref<4.0.0,>=3.6.0
28
30
  Requires-Dist: urllib3<3.0.0,>=1.25.3
29
31
  Requires-Dist: python_dateutil<3.0.0,>=2.8.2
30
32
  Requires-Dist: aiohttp<4.0.0,>=3.8.4
@@ -49,6 +51,7 @@ Requires-Dist: pytest-asyncio==0.26.0; extra == "test"
49
51
  Requires-Dist: pytest-cov==6.1.1; extra == "test"
50
52
  Requires-Dist: python-dotenv==1.0.1; extra == "test"
51
53
  Requires-Dist: PyJWT==2.10.0; extra == "test"
54
+ Requires-Dist: httpx>=0.27.0; extra == "test"
52
55
  Provides-Extra: extra
53
56
  Requires-Dist: pandas<3.0.0,>=2.2.0; extra == "extra"
54
57
  Dynamic: license-file
@@ -78,11 +81,15 @@ You can install extra dependencies grouped in the extras `extra`, `dev` (develop
78
81
 
79
82
  ## Structure
80
83
 
81
- Our API is available as an asynchronous Python SDK. The main entry point you need is the `ApiClient` class, which you would import like this:
84
+ Our API is available as both an asynchronous and synchronous Python SDK. The main entry points are:
85
+
86
+ - `AsyncClient` - Asynchronous client for async/await usage
87
+ - `SyncClient` - Synchronous client for traditional blocking calls
88
+
82
89
  ```python
83
- from crypticorn import ApiClient
90
+ from crypticorn import AsyncClient, SyncClient
84
91
  ```
85
- The ApiClient serves as the central interface for API operations. It instantiates multiple API wrappers corresponding to our micro services. These are structured the following:
92
+ Both clients serve as the central interface for API operations and instantiate multiple API wrappers corresponding to our micro services. These are structured the following:
86
93
 
87
94
  <img src="static/pip-structure.svg" alt="pip package structure" />
88
95
 
@@ -110,20 +117,22 @@ There are scopes which don't follow this structure. Those are either scopes that
110
117
 
111
118
  ## Basic Usage
112
119
 
113
- You can use the client with the async context protocol...
120
+ ### Asynchronous Client
121
+
122
+ You can use the async client with the context manager protocol...
114
123
  ```python
115
- async with ApiClient(api_key="your-api-key") as client:
116
- await client.pay.products.get_products()
124
+ async with AsyncClient(api_key="your-api-key") as client:
125
+ await client.pay.products.get_products()
117
126
  ```
118
127
  ...or without it like this...
119
128
  ```python
120
- client = ApiClient(api_key="your-api-key")
121
- asyncio.run(client.pay.models.get_products())
129
+ client = AsyncClient(api_key="your-api-key")
130
+ asyncio.run(client.pay.products.get_products())
122
131
  asyncio.run(client.close())
123
132
  ```
124
133
  ...or this.
125
134
  ```python
126
- client = ApiClient(api_key="your-api-key")
135
+ client = AsyncClient(api_key="your-api-key")
127
136
 
128
137
  async def main():
129
138
  await client.pay.products.get_products()
@@ -132,6 +141,29 @@ asyncio.run(main())
132
141
  asyncio.run(client.close())
133
142
  ```
134
143
 
144
+ ### Synchronous Client
145
+
146
+ For traditional synchronous usage without async/await, use the `SyncClient`:
147
+
148
+ ```python
149
+ from crypticorn import SyncClient
150
+
151
+ # With context manager (recommended)
152
+ with SyncClient(api_key="your-api-key") as client:
153
+ products = client.pay.products.get_products()
154
+ status = client.trade.status.ping()
155
+
156
+ # Or without context manager
157
+ client = SyncClient(api_key="your-api-key")
158
+ try:
159
+ products = client.pay.products.get_products()
160
+ status = client.trade.status.ping()
161
+ finally:
162
+ client.close() # Manual cleanup required
163
+ ```
164
+
165
+ The sync client provides the same API surface as the async client, but all methods return results directly instead of coroutines. Under the hood, it uses `asgiref.async_to_sync` to bridge async operations to synchronous calls, ensuring reliable operation without requiring async/await syntax.
166
+
135
167
  ## Response Types
136
168
 
137
169
  There are three different available output formats you can choose from:
@@ -139,7 +171,10 @@ There are three different available output formats you can choose from:
139
171
  ### Serialized Response
140
172
  You can get fully serialized responses as pydantic models. Using this, you get the full benefits of pydantic's type checking.
141
173
  ```python
174
+ # Async client
142
175
  res = await client.pay.products.get_products()
176
+ # Sync client
177
+ res = client.pay.products.get_products()
143
178
  print(res)
144
179
  ```
145
180
  The output would look like this:
@@ -149,7 +184,10 @@ The output would look like this:
149
184
 
150
185
  ### Serialized Response with HTTP Info
151
186
  ```python
187
+ # Async client
152
188
  res = await client.pay.products.get_products_with_http_info()
189
+ # Sync client
190
+ res = client.pay.products.get_products_with_http_info()
153
191
  print(res)
154
192
  ```
155
193
  The output would look like this:
@@ -170,8 +208,12 @@ print(res.headers)
170
208
  ### JSON Response
171
209
  You can receive a classical JSON response by suffixing the function name with `_without_preload_content`
172
210
  ```python
211
+ # Async client
173
212
  response = await client.pay.products.get_products_without_preload_content()
174
213
  print(await response.json())
214
+
215
+ # Sync client - Note: use regular methods instead as response.json() returns a coroutine
216
+ response = client.pay.products.get_products_without_preload_content()
175
217
  ```
176
218
  The output would look like this:
177
219
  ```python
@@ -201,33 +243,36 @@ To override e.g. the host for the Hive client to connect to localhost:8000 inste
201
243
  from crypticorn.hive import Configuration as HiveConfig
202
244
  from crypticorn.common import Service
203
245
 
204
- async with ApiClient() as client:
246
+ # Async client
247
+ async with AsyncClient() as client:
248
+ client.configure(config=HiveConfig(host="http://localhost:8000"), service=Service.HIVE)
249
+
250
+ # Sync client
251
+ with SyncClient() as client:
205
252
  client.configure(config=HiveConfig(host="http://localhost:8000"), service=Service.HIVE)
206
253
  ```
207
254
 
208
255
  ### Session Management
209
256
 
210
- By default, `ApiClient` manages a single shared `aiohttp.ClientSession` for all service wrappers.
257
+ By default, `AsyncClient` manages a single shared `aiohttp.ClientSession` for all service wrappers.
211
258
  However, you can pass your own pre-configured `aiohttp.ClientSession` if you need advanced control — for example, to add retries, custom headers, logging, or mocking behavior.
212
259
 
213
260
  When you inject a custom session, you are responsible for managing its lifecycle, including closing when you're done.
214
261
 
215
262
  ```python
216
263
  import aiohttp
217
- from crypticorn import ApiClient
264
+ from crypticorn import AsyncClient
218
265
 
219
266
  async def main():
220
267
  custom_session = aiohttp.ClientSession()
221
- async with ApiClient(api_key="your-key", http_client=custom_session) as client:
268
+ async with AsyncClient(api_key="your-key", http_client=custom_session) as client:
222
269
  await client.trade.status.ping()
223
270
  await custom_session.close()
224
- # or
225
- custom_session = aiohttp.ClientSession()
226
- client = ApiClient(api_key="your-key", http_client=custom_session)
227
- await client.trade.status.ping()
228
- await custom_session.close()
271
+
229
272
  ```
230
- If you don’t pass a session, `ApiClient` will create and manage one internally. In that case, it will be automatically closed when using `async with` or when calling `await client.close()` manually.
273
+ If you don’t pass a session, `AsyncClient` will create and manage one internally. In that case, it will be automatically closed when using `async with` or when calling `await client.close()` manually.
274
+
275
+ **Note on Sync Client**: The `SyncClient` uses per-operation sessions (creates and closes a session for each API call) to ensure reliable synchronous behavior. Custom sessions are accepted but not used. This approach prevents event loop conflicts at the cost of slightly higher overhead per operation.
231
276
 
232
277
  ### Disable Logging
233
278