orionapi 2.23.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.23.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.23.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
@@ -2748,33 +2781,68 @@ class EclipseV1(EclipseBase):
2748
2781
  self.wait_for_analytics()
2749
2782
 
2750
2783
  def get_orders(self):
2751
- """Get all completed (non-pending) trade orders.
2784
+ """Deprecated alias of ``get_trades(is_pending=False)``.
2785
+
2786
+ Both hit the same ``/tradeorder/trades`` endpoint; :meth:`get_trades` is the
2787
+ superset and also exposes the portfolio, block, filter and account filters.
2788
+ Emits a ``DeprecationWarning`` and will be removed in a future major release.
2752
2789
 
2753
2790
  Returns:
2754
2791
  list: List of completed trade order dicts
2755
2792
  """
2756
- return self.api_request(f"{self.base_url}/tradeorder/trades?isPending=false").json()
2793
+ warnings.warn(
2794
+ "get_orders is deprecated; use get_trades(is_pending=False).",
2795
+ DeprecationWarning,
2796
+ stacklevel=2,
2797
+ )
2798
+ return self.get_trades(is_pending=False)
2757
2799
 
2758
2800
  def get_orders_pending(self):
2759
- """Get all pending trade orders.
2801
+ """Deprecated alias of ``get_trades(is_pending=True)``.
2802
+
2803
+ Both hit the same ``/tradeorder/trades`` endpoint; :meth:`get_trades` is the
2804
+ superset and also exposes the portfolio, block, filter and account filters.
2805
+ Emits a ``DeprecationWarning`` and will be removed in a future major release.
2760
2806
 
2761
2807
  Returns:
2762
2808
  list: List of pending trade order dicts
2763
2809
  """
2764
- return self.api_request(f"{self.base_url}/tradeorder/trades?isPending=true").json()
2810
+ warnings.warn(
2811
+ "get_orders_pending is deprecated; use get_trades(is_pending=True).",
2812
+ DeprecationWarning,
2813
+ stacklevel=2,
2814
+ )
2815
+ return self.get_trades(is_pending=True)
2765
2816
 
2766
- def get_trades(self, portfolio_id=None, top=None, is_pending=None):
2767
- """Get trade orders with optional portfolio / paging / pending filters.
2817
+ def get_trades(
2818
+ self,
2819
+ portfolio_id=None,
2820
+ top=None,
2821
+ is_pending=None,
2822
+ block_id=None,
2823
+ filter_id=None,
2824
+ account_ids=None,
2825
+ ):
2826
+ """Get trade orders with optional portfolio / block / filter / pending filters.
2768
2827
 
2769
- Hits the same ``/tradeorder/trades`` endpoint as :meth:`get_orders` /
2770
- :meth:`get_orders_pending`, but lets the caller filter by portfolio and cap
2771
- the result count.
2828
+ The canonical accessor for ``GET /tradeorder/trades``. Supersedes the
2829
+ deprecated :meth:`get_orders` / :meth:`get_orders_pending`, which only
2830
+ varied ``is_pending``.
2772
2831
 
2773
2832
  Args:
2774
2833
  portfolio_id: Optional portfolio ID filter (maps to ``portfolioId``)
2775
- top: Optional max number of results (maps to ``$top``)
2834
+ top: Deprecated and ignored. ``$top`` is not a documented param on this
2835
+ endpoint, and Eclipse silently drops unknown query params rather
2836
+ than erroring — live-verified 2026-07-28: with 10 trades in the
2837
+ tenant, ``$top=3`` still returned all 10. Slice the result instead.
2776
2838
  is_pending: Optional bool; when set, filter by pending status
2777
2839
  (maps to ``isPending``)
2840
+ block_id: Optional trade block ID filter (maps to ``blockId``)
2841
+ filter_id: Optional trade filter type ID (maps to ``filterId``); see
2842
+ the ``/tradeorder/trades/filterTypes`` endpoint for valid values
2843
+ account_ids: Optional account ID filter (maps to ``accountIds``);
2844
+ accepts a list/tuple of IDs, which is joined to a comma-separated
2845
+ string, or a pre-formatted string
2778
2846
 
2779
2847
  Returns:
2780
2848
  list: Trade order dicts
@@ -2783,12 +2851,246 @@ class EclipseV1(EclipseBase):
2783
2851
  if portfolio_id is not None:
2784
2852
  params["portfolioId"] = portfolio_id
2785
2853
  if top is not None:
2786
- params["$top"] = top
2854
+ warnings.warn(
2855
+ "get_trades(top=...) is ignored by Eclipse: $top is not a supported "
2856
+ "param on /tradeorder/trades and the server silently drops it. "
2857
+ "Slice the returned list instead. This parameter will be removed in "
2858
+ "a future major release.",
2859
+ DeprecationWarning,
2860
+ stacklevel=2,
2861
+ )
2787
2862
  if is_pending is not None:
2788
2863
  params["isPending"] = str(is_pending).lower()
2864
+ if block_id is not None:
2865
+ params["blockId"] = block_id
2866
+ if filter_id is not None:
2867
+ params["filterId"] = filter_id
2868
+ if account_ids is not None:
2869
+ if isinstance(account_ids, (list, tuple)):
2870
+ account_ids = ",".join(str(a) for a in account_ids)
2871
+ params["accountIds"] = account_ids
2789
2872
  res = self.api_request(f"{self.base_url}/tradeorder/trades", params=params)
2790
2873
  return res.json()
2791
2874
 
2875
+ def validate_trade(
2876
+ self,
2877
+ action_id,
2878
+ account_id=None,
2879
+ portfolio_id=None,
2880
+ security_id=None,
2881
+ dollar_amount=None,
2882
+ quantity=None,
2883
+ percentage=None,
2884
+ is_auto_allocate=None,
2885
+ ):
2886
+ """Validate a trade before creating it (``POST /tradeorder/trades/validate``).
2887
+
2888
+ Writes nothing. Use this as the pre-flight check for :meth:`create_trade`,
2889
+ which has no preview / view-only mode of its own.
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
+
2899
+ Args:
2900
+ action_id: 1=Buy, 2=Sell, 3=Journal In, 4=Journal Out
2901
+ account_id: Optional account ID
2902
+ portfolio_id: Optional portfolio ID
2903
+ security_id: Optional security ID
2904
+ dollar_amount: Optional dollar amount to buy/sell
2905
+ quantity: Optional number of shares to buy/sell
2906
+ percentage: Optional percent of the position to buy/sell
2907
+ is_auto_allocate: Optional bool (maps to ``isAutoAllocate``)
2908
+
2909
+ Returns:
2910
+ dict: ``message``, ``tradeAmount``, ``cashValuePostTrade`` and a
2911
+ ``warningMessage`` list (e.g. warnings about overspending cash)
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
+ )
2919
+ payload = {
2920
+ "actionId": action_id,
2921
+ "accountId": account_id,
2922
+ "portfolioId": portfolio_id,
2923
+ "securityId": security_id,
2924
+ "dollarAmount": dollar_amount,
2925
+ "quantity": quantity,
2926
+ "percentage": percentage,
2927
+ "isAutoAllocate": is_auto_allocate,
2928
+ }
2929
+ payload = {k: v for k, v in payload.items() if v is not None}
2930
+
2931
+ res = self.api_request(
2932
+ f"{self.base_url}/tradeorder/trades/validate", requests.post, json=payload
2933
+ )
2934
+ return res.json()
2935
+
2936
+ def create_trade(
2937
+ self,
2938
+ action_id,
2939
+ trade_tool_selection,
2940
+ trade_instance_type,
2941
+ trade_instance_sub_type,
2942
+ account_id=None,
2943
+ portfolio_id=None,
2944
+ security_id=None,
2945
+ dollar_amount=None,
2946
+ quantity=None,
2947
+ percentage=None,
2948
+ sync=True,
2949
+ ):
2950
+ """Generate trade orders (``POST /tradeorder/trades``).
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
+
2972
+ The generic per-trade creator behind Eclipse's Quick Trade / journal flows,
2973
+ as opposed to the ``/tradetool/`` generators (:meth:`cash_needs_trade`,
2974
+ :meth:`rebalance_trade`, ...) which operate on whole portfolios.
2975
+
2976
+ .. warning::
2977
+ This endpoint has **no view-only mode** — it always writes. Call
2978
+ :meth:`validate_trade` first to preview the trade amount, the resulting
2979
+ cash balance and any warnings.
2980
+
2981
+ Trades are always created as pending orders: the wrapper hardcodes
2982
+ ``isSendImmediately: false`` and deliberately does not expose it, so a
2983
+ single call can never both create and execute a trade. Review and
2984
+ execute the resulting orders in Eclipse.
2985
+
2986
+ Args:
2987
+ action_id: 1=Buy, 2=Sell, 3=Journal In, 4=Journal Out. (The Eclipse
2988
+ docs list both journal directions as ``3``; ``4`` is accepted on
2989
+ the assumption that Journal Out is ``4``.)
2990
+ trade_tool_selection: 1=Portfolio, 2=Account, 3=Model,
2991
+ 4=Account Trade Group, 5=Sleeve Portfolio, 6=Excel Import,
2992
+ 7=Portfolio Trade Group, 8=Security Import, 9=Individual Sleeve,
2993
+ 10=Tactical Tool
2994
+ trade_instance_type: Trade instance type ID; see
2995
+ :data:`TRADE_INSTANCE_TYPES`
2996
+ trade_instance_sub_type: Trade instance sub-type ID; see
2997
+ :data:`TRADE_INSTANCE_SUBTYPES`
2998
+ account_id: Account ID (required unless ``portfolio_id`` is given)
2999
+ portfolio_id: Portfolio ID (required unless ``account_id`` is given)
3000
+ security_id: Security ID; required for buys and sells
3001
+ dollar_amount: Dollar amount to buy/sell
3002
+ quantity: Number of shares to buy/sell
3003
+ percentage: Percent of the position to buy/sell
3004
+ sync: Wait for analytics to complete (default True)
3005
+
3006
+ Returns:
3007
+ dict: Either ``{"message": ...}`` or ``{"issues": [...],
3008
+ "success": [...], "instanceId": ...}``
3009
+
3010
+ Raises:
3011
+ ValueError: If the enum values are unknown, option trading is
3012
+ requested (unsupported by Eclipse), neither an account nor a
3013
+ portfolio is given, ``quantity`` is used (silently discarded
3014
+ upstream), or a buy/sell does not specify exactly one of
3015
+ ``dollar_amount`` / ``quantity`` / ``percentage``.
3016
+ OrionAPIError: If Eclipse reports ``"No Trade is generated."`` -- a
3017
+ 200 response that created nothing.
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
+ )
3032
+ if action_id not in (1, 2, 3, 4):
3033
+ raise ValueError(
3034
+ "action_id must be 1 (Buy), 2 (Sell), 3 (Journal In) or 4 (Journal Out)"
3035
+ )
3036
+ if trade_tool_selection not in range(1, 11):
3037
+ raise ValueError("trade_tool_selection must be an integer between 1 and 10")
3038
+ if trade_instance_type not in TRADE_INSTANCE_TYPES:
3039
+ raise ValueError(
3040
+ f"trade_instance_type must be one of {sorted(TRADE_INSTANCE_TYPES)}, "
3041
+ f"got {trade_instance_type!r}"
3042
+ )
3043
+ if trade_instance_sub_type not in TRADE_INSTANCE_SUBTYPES:
3044
+ raise ValueError(
3045
+ f"trade_instance_sub_type must be one of {sorted(TRADE_INSTANCE_SUBTYPES)}, "
3046
+ f"got {trade_instance_sub_type!r}"
3047
+ )
3048
+ # Eclipse documents option trading as "not supported" on this endpoint.
3049
+ if trade_instance_type == 4 or trade_instance_sub_type == 9:
3050
+ raise ValueError("Option Trading is not supported by the /tradeorder/trades endpoint")
3051
+ if account_id is None and portfolio_id is None:
3052
+ raise ValueError("at least one of account_id or portfolio_id is required")
3053
+
3054
+ is_buy_or_sell = action_id in (1, 2)
3055
+ if is_buy_or_sell:
3056
+ if security_id is None:
3057
+ raise ValueError("security_id is required for buys and sells")
3058
+ amounts = [a for a in (dollar_amount, quantity, percentage) if a]
3059
+ if len(amounts) != 1:
3060
+ raise ValueError(
3061
+ "exactly one of dollar_amount, quantity or percentage must be set "
3062
+ "for buys and sells"
3063
+ )
3064
+
3065
+ # Buys/sells send 0 for the unused amount fields; journals send null.
3066
+ unset = 0 if is_buy_or_sell else None
3067
+ payload = {
3068
+ "accountId": account_id,
3069
+ "portfolioId": portfolio_id,
3070
+ "actionId": action_id,
3071
+ "securityId": security_id,
3072
+ "dollarAmount": dollar_amount if dollar_amount is not None else unset,
3073
+ "quantity": quantity if quantity is not None else unset,
3074
+ "percentage": percentage if percentage is not None else unset,
3075
+ # Never expose this: creating and executing a trade must stay separate.
3076
+ "isSendImmediately": False,
3077
+ "tradeToolSelection": trade_tool_selection,
3078
+ "tradeInstanceType": trade_instance_type,
3079
+ "tradeInstanceSubType": trade_instance_sub_type,
3080
+ }
3081
+
3082
+ res = self.api_request(f"{self.base_url}/tradeorder/trades", requests.post, json=payload)
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
+ )
3091
+ self._maybe_wait_for_analytics(sync)
3092
+ return result
3093
+
2792
3094
  def cash_needs_trade(
2793
3095
  self,
2794
3096
  portfolio_ids,
@@ -6834,6 +7136,287 @@ class EclipseV2(EclipseBase):
6834
7136
  res = self.api_request(f"{self.base_url_v2}/Trading/ActiveBatchJobs", params=params)
6835
7137
  return res.json()
6836
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
+
6837
7420
  # --- Optimization (v2-only): per-batch / per-account detail reads ------------
6838
7421
 
6839
7422
  def get_optimization_accounts(self, batch_id):
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "orionapi"
3
- version = "2.23.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