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,4 +1,4 @@
|
|
|
1
|
-
__version__ = "2.
|
|
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
|
-
"""
|
|
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
|
-
|
|
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
|
-
"""
|
|
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
|
-
|
|
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(
|
|
2767
|
-
|
|
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
|
-
|
|
2770
|
-
:meth:`get_orders_pending`,
|
|
2771
|
-
|
|
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:
|
|
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
|
-
|
|
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):
|
|
File without changes
|