orionapi 2.24.0__tar.gz → 2.26.0__tar.gz

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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: orionapi
3
- Version: 2.24.0
3
+ Version: 2.26.0
4
4
  Summary: A python interface for the Orion Advisors platform web API
5
5
  Author: spencerogden-dsam
6
6
  Author-email: 67068943+spencerogden-dsam@users.noreply.github.com
@@ -1,4 +1,4 @@
1
- __version__ = "2.24.0"
1
+ __version__ = "2.26.0"
2
2
 
3
3
  import logging
4
4
  import re
@@ -55,6 +55,39 @@ TRADE_INSTANCE_SUBTYPES = {
55
55
  22: "Liquidate",
56
56
  }
57
57
 
58
+ # TradingApplication mappings for the v2 /TradeOrder/Trades endpoints.
59
+ # Not published in the Swagger (which lists the ints with no names) -- recovered
60
+ # 2026-07-28 from the API's own rejection messages ("Trade Application <Name> is
61
+ # not supported."). Only MANUAL_TRADE (11) actually creates trade orders; see
62
+ # EclipseV2.create_trades.
63
+ TRADING_APPLICATIONS = {
64
+ 0: "BuySell",
65
+ 1: "CashNeeds",
66
+ 2: "GlobalTrades",
67
+ 3: "ProrateCash",
68
+ 4: "RaiseCash",
69
+ 5: "Rebalance",
70
+ 6: "SpendCash",
71
+ 7: "(unnamed)",
72
+ 8: "TaxLossHarvesting",
73
+ 9: "TickerSwap",
74
+ 10: "TradeToTargetPercent",
75
+ 11: "ManualTrade",
76
+ 12: "TacticalTrades",
77
+ 13: "PartialShare",
78
+ 14: "(unnamed, accepted but creates nothing)",
79
+ 15: "TradeImport",
80
+ 16: "RaiseCashByFund",
81
+ 17: "OptionTrades",
82
+ 18: "OutsourcedTradeExecution",
83
+ }
84
+
85
+ # The only TradingApplication value that creates trade orders on
86
+ # POST /api/v2/TradeOrder/Trades. Every other value except 14 returns
87
+ # 400 "Trade Application <Name> is not supported."; 14 returns 200 with an
88
+ # empty ``tradeId`` list and creates nothing. Live-verified 2026-07-28.
89
+ TRADING_APPLICATION_MANUAL = 11
90
+
58
91
  # Set-Aside Cash Constants (from Eclipse API)
59
92
  CASH_TYPE_DOLLAR = 1
60
93
  CASH_TYPE_PERCENT = 2
@@ -2855,6 +2888,14 @@ class EclipseV1(EclipseBase):
2855
2888
  Writes nothing. Use this as the pre-flight check for :meth:`create_trade`,
2856
2889
  which has no preview / view-only mode of its own.
2857
2890
 
2891
+ .. deprecated:: 2.26.0
2892
+ Use :meth:`EclipseV2.validate_trades` instead; this emits a
2893
+ ``DeprecationWarning``. ``action_id=2`` returns HTTP 500 while a
2894
+ byte-identical buy succeeds — the same upstream defect that affects
2895
+ :meth:`create_trade`. Pairing this with the deprecated
2896
+ :meth:`create_trade` also means validating and creating on the same
2897
+ unreliable surface.
2898
+
2858
2899
  Args:
2859
2900
  action_id: 1=Buy, 2=Sell, 3=Journal In, 4=Journal Out
2860
2901
  account_id: Optional account ID
@@ -2869,6 +2910,12 @@ class EclipseV1(EclipseBase):
2869
2910
  dict: ``message``, ``tradeAmount``, ``cashValuePostTrade`` and a
2870
2911
  ``warningMessage`` list (e.g. warnings about overspending cash)
2871
2912
  """
2913
+ warnings.warn(
2914
+ "EclipseV1.validate_trade is deprecated: the endpoint 500s on every sell. "
2915
+ "Use EclipseV2.validate_trades (Eclipse().validate_trades).",
2916
+ DeprecationWarning,
2917
+ stacklevel=2,
2918
+ )
2872
2919
  payload = {
2873
2920
  "actionId": action_id,
2874
2921
  "accountId": account_id,
@@ -2902,6 +2949,26 @@ class EclipseV1(EclipseBase):
2902
2949
  ):
2903
2950
  """Generate trade orders (``POST /tradeorder/trades``).
2904
2951
 
2952
+ .. deprecated:: 2.26.0
2953
+ Use :meth:`EclipseV2.create_trades` instead; this emits a
2954
+ ``DeprecationWarning``. The endpoint is unreliable in two independent
2955
+ ways -- only ``dollar_amount`` buys actually work. All live-verified
2956
+ 2026-07-28 and reported to Orion:
2957
+
2958
+ =================== ===============================================
2959
+ Input Result
2960
+ =================== ===============================================
2961
+ ``action_id=2`` HTTP 500 for **every** sell, across 5 securities,
2962
+ all amount modes and every documented body shape
2963
+ ``quantity=N`` HTTP 200 ``"No Trade is generated."`` -- creates
2964
+ nothing; this wrapper now rejects it up front
2965
+ ``percentage=N`` May create a zero-quantity, *disabled* order
2966
+ ``dollar_amount=N`` Works
2967
+ =================== ===============================================
2968
+
2969
+ :meth:`EclipseV2.create_trades` handles buys *and* sells and honours
2970
+ share count and dollar amount exactly.
2971
+
2905
2972
  The generic per-trade creator behind Eclipse's Quick Trade / journal flows,
2906
2973
  as opposed to the ``/tradetool/`` generators (:meth:`cash_needs_trade`,
2907
2974
  :meth:`rebalance_trade`, ...) which operate on whole portfolios.
@@ -2943,9 +3010,25 @@ class EclipseV1(EclipseBase):
2943
3010
  Raises:
2944
3011
  ValueError: If the enum values are unknown, option trading is
2945
3012
  requested (unsupported by Eclipse), neither an account nor a
2946
- portfolio is given, or a buy/sell does not specify exactly one of
3013
+ portfolio is given, ``quantity`` is used (silently discarded
3014
+ upstream), or a buy/sell does not specify exactly one of
2947
3015
  ``dollar_amount`` / ``quantity`` / ``percentage``.
3016
+ OrionAPIError: If Eclipse reports ``"No Trade is generated."`` -- a
3017
+ 200 response that created nothing.
2948
3018
  """
3019
+ warnings.warn(
3020
+ "EclipseV1.create_trade is deprecated: the endpoint 500s on every sell and "
3021
+ "silently discards share-quantity buys. Use EclipseV2.create_trades "
3022
+ "(Eclipse().create_trades), which handles both and honours shares/amount.",
3023
+ DeprecationWarning,
3024
+ stacklevel=2,
3025
+ )
3026
+ if quantity is not None:
3027
+ raise ValueError(
3028
+ "quantity is silently ignored by this endpoint -- it returns 200 with "
3029
+ '"No Trade is generated." and creates nothing. Use dollar_amount here, '
3030
+ "or EclipseV2.create_trades(build_trade(..., shares=N)) for share counts."
3031
+ )
2949
3032
  if action_id not in (1, 2, 3, 4):
2950
3033
  raise ValueError(
2951
3034
  "action_id must be 1 (Buy), 2 (Sell), 3 (Journal In) or 4 (Journal Out)"
@@ -2998,6 +3081,13 @@ class EclipseV1(EclipseBase):
2998
3081
 
2999
3082
  res = self.api_request(f"{self.base_url}/tradeorder/trades", requests.post, json=payload)
3000
3083
  result = res.json()
3084
+ # A 200 carrying this message means nothing was created. Do not let a
3085
+ # silent no-op look like a successful write.
3086
+ if isinstance(result, dict) and result.get("message") == "No Trade is generated.":
3087
+ raise OrionAPIError(
3088
+ "Eclipse returned 'No Trade is generated.' — the request succeeded but "
3089
+ "created no trade orders. Use EclipseV2.create_trades instead."
3090
+ )
3001
3091
  self._maybe_wait_for_analytics(sync)
3002
3092
  return result
3003
3093
 
@@ -7046,6 +7136,287 @@ class EclipseV2(EclipseBase):
7046
7136
  res = self.api_request(f"{self.base_url_v2}/Trading/ActiveBatchJobs", params=params)
7047
7137
  return res.json()
7048
7138
 
7139
+ # --- TradeOrder / Trades (v2): batch create + validate -----------------------
7140
+ # The v2 counterparts of EclipseV1.create_trade / validate_trade. Named in the
7141
+ # plural because v2 takes a *list* of trades in one wrapper -- which also keeps
7142
+ # them distinct from the v1 singular methods on the Eclipse unifier, whose
7143
+ # __getattr__ resolves v1 first.
7144
+ #
7145
+ # Prefer these over the v1 pair for SELLS: the v1 endpoints return HTTP 500 for
7146
+ # every sell (see EclipseV1.create_trade's docstring). Live-verified 2026-07-28:
7147
+ # v2 validates sells correctly, returning isValid/tradeAmount/tradeShares.
7148
+
7149
+ @staticmethod
7150
+ def build_trade(
7151
+ account_id,
7152
+ portfolio_id,
7153
+ security_id,
7154
+ action,
7155
+ shares=None,
7156
+ amount=None,
7157
+ percent=None,
7158
+ **extra,
7159
+ ):
7160
+ """Build one entry for the ``trades`` list of :meth:`validate_trades` /
7161
+ :meth:`create_trades`.
7162
+
7163
+ Saves the caller from having to know the v2 ``ValidateTradeDto`` key names.
7164
+
7165
+ Exactly one of ``shares`` / ``amount`` / ``percent`` sets the trade size:
7166
+
7167
+ ========= =================== =================================================
7168
+ Parameter Sent as Meaning
7169
+ ========= =================== =================================================
7170
+ ``shares`` ``tradeShares`` **Share count.** A whole or fractional number of
7171
+ shares. Honoured exactly -- live-verified
7172
+ ``shares=1`` produced an order of qty 1.0.
7173
+ ``amount`` ``tradeAmount`` **Dollar amount.** Eclipse divides by the current
7174
+ price and books the resulting (fractional) share
7175
+ count -- ``amount=500`` on a $393 stock produced
7176
+ qty 1.2711.
7177
+ ``percent`` ``tradePercent`` Percent of the *existing position*. Only
7178
+ meaningful for sells/trims, and a percent that
7179
+ rounds to <1 share can yield a zero-quantity,
7180
+ disabled order. Prefer ``shares`` or ``amount``.
7181
+ ========= =================== =================================================
7182
+
7183
+ Args:
7184
+ account_id: Account ID (``accountId``)
7185
+ portfolio_id: Portfolio ID (``portfolioId``). **Required** -- see the
7186
+ warning on :meth:`validate_trades`; omitting it makes the API
7187
+ silently return an empty list.
7188
+ security_id: Security ID (``securityId``)
7189
+ action: 1=Buy, 2=Sell, 3=Journal In, 4=Journal Out (``action``)
7190
+ shares: Number of shares to buy/sell (``tradeShares``) -- preferred
7191
+ amount: Dollar amount to buy/sell (``tradeAmount``) -- preferred
7192
+ percent: Percent of the position (``tradePercent``); see caveat above
7193
+ **extra: Any other ``ValidateTradeDto`` field, passed through verbatim
7194
+
7195
+ Returns:
7196
+ dict: A single trade entry
7197
+
7198
+ Raises:
7199
+ ValueError: If ``portfolio_id`` is missing, ``action`` is out of range,
7200
+ or not exactly one of ``shares`` / ``amount`` / ``percent`` is set.
7201
+
7202
+ Example:
7203
+ >>> e.build_trade(account_id=686, portfolio_id=209, security_id=73,
7204
+ ... action=1, shares=1)
7205
+ {'accountId': 686, 'portfolioId': 209, 'securityId': 73,
7206
+ 'action': 1, 'tradeShares': 1}
7207
+ """
7208
+ if portfolio_id is None:
7209
+ raise ValueError(
7210
+ "portfolio_id is required; without portfolioId the v2 trade endpoints "
7211
+ "return an empty list instead of validating"
7212
+ )
7213
+ if action not in (1, 2, 3, 4):
7214
+ raise ValueError("action must be 1 (Buy), 2 (Sell), 3 (Journal In) or 4 (Journal Out)")
7215
+ sizes = [s for s in (shares, amount, percent) if s]
7216
+ if len(sizes) != 1:
7217
+ raise ValueError("exactly one of shares, amount or percent must be set")
7218
+
7219
+ trade = {
7220
+ "accountId": account_id,
7221
+ "portfolioId": portfolio_id,
7222
+ "securityId": security_id,
7223
+ "action": action,
7224
+ }
7225
+ if shares is not None:
7226
+ trade["tradeShares"] = shares
7227
+ if amount is not None:
7228
+ trade["tradeAmount"] = amount
7229
+ if percent is not None:
7230
+ trade["tradePercent"] = percent
7231
+ trade.update(extra)
7232
+ return trade
7233
+
7234
+ def _trade_wrapper(
7235
+ self,
7236
+ trades,
7237
+ application,
7238
+ trade_tool_selection,
7239
+ trade_instance_type,
7240
+ trade_instance_sub_type,
7241
+ instance_notes,
7242
+ ):
7243
+ """Build and sanity-check the ``ValidateTradeWrapperDto`` body."""
7244
+ if not trades:
7245
+ raise ValueError("trades must be a non-empty list of trade dicts")
7246
+ for i, t in enumerate(trades):
7247
+ if t.get("portfolioId") is None:
7248
+ raise ValueError(
7249
+ f"trades[{i}] is missing portfolioId; without it the v2 trade "
7250
+ "endpoints silently return an empty list instead of validating. "
7251
+ "Use build_trade() to construct entries."
7252
+ )
7253
+ body = {
7254
+ "application": application,
7255
+ "trades": trades,
7256
+ "tradeToolSelection": trade_tool_selection,
7257
+ "tradeInstanceType": trade_instance_type,
7258
+ "tradeInstanceSubType": trade_instance_sub_type,
7259
+ }
7260
+ if instance_notes is not None:
7261
+ body["instanceNotes"] = instance_notes
7262
+ return body
7263
+
7264
+ def validate_trades(
7265
+ self,
7266
+ trades,
7267
+ application=TRADING_APPLICATION_MANUAL,
7268
+ trade_tool_selection=2,
7269
+ trade_instance_type=5,
7270
+ trade_instance_sub_type=11,
7271
+ instance_notes=None,
7272
+ ):
7273
+ """Validate a batch of trades (``POST /TradeOrder/Trades/Action/Validate``).
7274
+
7275
+ Writes nothing. The v2 counterpart of :meth:`EclipseV1.validate_trade`, and
7276
+ the one to use for **sells** -- the v1 endpoint 500s on every sell.
7277
+
7278
+ .. warning::
7279
+ Every entry in ``trades`` must carry ``portfolioId``. Without it the API
7280
+ returns ``200 []`` -- an empty list, not an error -- so an unvalidated
7281
+ batch looks indistinguishable from a clean one. This method raises
7282
+ ``ValueError`` rather than let that pass silently.
7283
+
7284
+ Note:
7285
+ The response is not 1:1 with the input. Eclipse expands each requested
7286
+ trade across the underlying tax lots / sleeve accounts, so one entry can
7287
+ come back as several rows (live-verified: a 99,999-share sell returned 4).
7288
+
7289
+ Args:
7290
+ trades: List of trade dicts; build them with :meth:`build_trade`.
7291
+ Size each with ``shares`` (share count) or ``amount`` (dollars) --
7292
+ see :meth:`build_trade` for the full parameter table.
7293
+ application: ``TradingApplication`` -- see :data:`TRADING_APPLICATIONS`.
7294
+ Defaults to 11 (ManualTrade), the only value that creates orders on
7295
+ :meth:`create_trades`. This endpoint tolerates other values, but
7296
+ keeping the two in step avoids validating under one application and
7297
+ creating under another.
7298
+ trade_tool_selection: 1=Portfolio, 2=Account, 3=Model, ... (default 2)
7299
+ trade_instance_type: See :data:`TRADE_INSTANCE_TYPES` (default 5, Quick Trade)
7300
+ trade_instance_sub_type: See :data:`TRADE_INSTANCE_SUBTYPES`
7301
+ (default 11, Quick Trade)
7302
+ instance_notes: Optional notes for the trade instance
7303
+
7304
+ Returns:
7305
+ list: Validated trade dicts with ``isValid``, ``tradeAmount``,
7306
+ ``tradeShares``, ``tradePercent``, ``warningMessage``,
7307
+ ``zeroTradeReasons`` and gain/loss detail
7308
+
7309
+ Raises:
7310
+ ValueError: If ``trades`` is empty or any entry lacks ``portfolioId``
7311
+ """
7312
+ body = self._trade_wrapper(
7313
+ trades,
7314
+ application,
7315
+ trade_tool_selection,
7316
+ trade_instance_type,
7317
+ trade_instance_sub_type,
7318
+ instance_notes,
7319
+ )
7320
+ res = self.api_request(
7321
+ f"{self.base_url_v2}/TradeOrder/Trades/Action/Validate", requests.post, json=body
7322
+ )
7323
+ return res.json()
7324
+
7325
+ def create_trades(
7326
+ self,
7327
+ trades,
7328
+ application=TRADING_APPLICATION_MANUAL,
7329
+ trade_tool_selection=2,
7330
+ trade_instance_type=5,
7331
+ trade_instance_sub_type=11,
7332
+ instance_notes=None,
7333
+ ):
7334
+ """Create a batch of trade orders (``POST /TradeOrder/Trades``).
7335
+
7336
+ **The recommended way to create trades.** Prefer this over
7337
+ :meth:`EclipseV1.create_trade`, which is deprecated: v1 500s on every sell
7338
+ and silently discards share-count buys.
7339
+
7340
+ Size each trade with **``shares`` (share count)** or **``amount``
7341
+ (dollars)** via :meth:`build_trade` -- both are honoured exactly here.
7342
+ Live-verified 2026-07-28: ``shares=1`` created an order of qty 1.0, and
7343
+ ``amount=500`` on a $393 stock created qty 1.2711.
7344
+
7345
+ .. warning::
7346
+ This writes. Call :meth:`validate_trades` first and check ``isValid`` /
7347
+ ``warningMessage`` on every returned row; the same ``portfolioId``
7348
+ requirement applies here.
7349
+
7350
+ Unlike the v1 endpoint there is no ``isSendImmediately`` field on this
7351
+ surface at all, so created orders are always staged as pending. Setting
7352
+ ``approvalStatus`` or ``orderStatus`` on a trade entry is rejected: that
7353
+ would amount to creating and approving in one call, which this wrapper
7354
+ deliberately does not allow. Review and execute in Eclipse.
7355
+
7356
+ Note:
7357
+ No ``sync`` parameter: the analytics-wait helpers live on
7358
+ :class:`EclipseV1`. Call ``Eclipse().v1.wait_for_analytics()`` if you
7359
+ need to block until analytics settle.
7360
+
7361
+ Args:
7362
+ trades: List of trade dicts; build them with :meth:`build_trade`.
7363
+ Size each with ``shares`` (share count) or ``amount`` (dollars);
7364
+ see :meth:`build_trade` for the full parameter table.
7365
+ application: ``TradingApplication`` -- see :data:`TRADING_APPLICATIONS`.
7366
+ **Only 11 (ManualTrade) creates orders**, and it is the default.
7367
+ Every other value except 14 returns 400 "Trade Application <Name>
7368
+ is not supported."; 14 returns 200 with an empty ``tradeId`` list
7369
+ and creates nothing, so it is rejected here rather than allowed to
7370
+ look like a success.
7371
+ trade_tool_selection: 1=Portfolio, 2=Account, 3=Model, ... (default 2)
7372
+ trade_instance_type: See :data:`TRADE_INSTANCE_TYPES` (default 5, Quick Trade)
7373
+ trade_instance_sub_type: See :data:`TRADE_INSTANCE_SUBTYPES`
7374
+ (default 11, Quick Trade)
7375
+ instance_notes: Optional notes for the trade instance
7376
+
7377
+ Returns:
7378
+ dict: ``instanceId``, ``tradeId`` (the created trade order IDs),
7379
+ ``trades`` and ``excludeTrades``
7380
+
7381
+ Raises:
7382
+ ValueError: If ``trades`` is empty, any entry lacks ``portfolioId``, any
7383
+ entry sets ``approvalStatus`` / ``orderStatus``, or ``application``
7384
+ is one that cannot create orders.
7385
+
7386
+ Example:
7387
+ >>> t = e.build_trade(account_id=686, portfolio_id=209, security_id=73,
7388
+ ... action=2, shares=1) # SELL 1 share
7389
+ >>> [r["isValid"] for r in e.validate_trades([t])]
7390
+ [True]
7391
+ >>> e.create_trades([t])["tradeId"]
7392
+ [72495]
7393
+ """
7394
+ if application != TRADING_APPLICATION_MANUAL:
7395
+ name = TRADING_APPLICATIONS.get(application, "unknown")
7396
+ raise ValueError(
7397
+ f"application={application} ({name}) does not create trade orders on "
7398
+ f"this endpoint; only {TRADING_APPLICATION_MANUAL} (ManualTrade) does. "
7399
+ "Other values return 400, and 14 returns 200 while creating nothing."
7400
+ )
7401
+ for i, t in enumerate(trades or []):
7402
+ for banned in ("approvalStatus", "orderStatus"):
7403
+ if t.get(banned) is not None:
7404
+ raise ValueError(
7405
+ f"trades[{i}] sets {banned}; creating and approving/executing a "
7406
+ "trade in one call is not supported by this wrapper. Create the "
7407
+ "orders here, then action them in Eclipse."
7408
+ )
7409
+ body = self._trade_wrapper(
7410
+ trades,
7411
+ application,
7412
+ trade_tool_selection,
7413
+ trade_instance_type,
7414
+ trade_instance_sub_type,
7415
+ instance_notes,
7416
+ )
7417
+ res = self.api_request(f"{self.base_url_v2}/TradeOrder/Trades", requests.post, json=body)
7418
+ return res.json()
7419
+
7049
7420
  # --- Optimization (v2-only): per-batch / per-account detail reads ------------
7050
7421
 
7051
7422
  def get_optimization_accounts(self, batch_id):
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "orionapi"
3
- version = "2.24.0"
3
+ version = "2.26.0"
4
4
  description = "A python interface for the Orion Advisors platform web API"
5
5
  authors = ["spencerogden-dsam <67068943+spencerogden-dsam@users.noreply.github.com>"]
6
6
 
File without changes