Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
3611006
feature: add etoro brokerage name and order properties
Romazes Oct 9, 2026
3a973e5
feature: add etoro fee model
Romazes Oct 9, 2026
d2fc417
feature: add etoro brokerage model
Romazes Oct 9, 2026
d8ffe40
feature: add etoro keys and live environment to config
Romazes Oct 9, 2026
5038d57
test: add etoro brokerage model and fee model tests
Romazes Oct 9, 2026
8b1c91d
refactor: use the lean update message in the etoro brokerage model
Romazes Oct 9, 2026
e9a9a49
refactor: drop the etoro demo environment key from the launcher config
Romazes Oct 12, 2026
1b0e0f2
feature: bring back the etoro demo environment key in the launcher co…
Romazes Oct 13, 2026
4fb0c34
fix: brokerage order tests name the invalid reason on a status timeout
Romazes Oct 14, 2026
7fbad03
fix: the etoro brokerage model accepts currency pairs
Romazes Oct 19, 2026
73e1416
fix: etoro brokerage model gives every security leverage 1
Romazes Oct 23, 2026
2e3430b
fix: etoro model defaults to a cash account, leverage needs a stop
Romazes Nov 3, 2026
38d8a74
fix: etoro crypto fee in the quote currency of the pair
Romazes Nov 3, 2026
a7160e3
feature: a contingent order set exposes its absolute quantities
Romazes Nov 5, 2026
64c0614
feature: etoro stop loss and take profit as contingent exit orders
Romazes Nov 5, 2026
fc768d7
feature: etoro model keeps a crypto stop loss at least 10% from the p…
Romazes Nov 6, 2026
cc2987c
test: etoro model counts as supporting contingent orders
Romazes Nov 6, 2026
8921806
feature: etoro model updates a stop market or a limit order by type
Romazes Nov 6, 2026
ada43a3
test: brokerage test harness applies a quantity update
Romazes Nov 6, 2026
46f6c5e
fix: etoro model refuses an exit order on the side of its entry
Romazes Nov 9, 2026
74e4977
refactor: etoro order properties type removed until it has a field
Romazes Nov 9, 2026
5334ab3
fix: etoro model starts a currency pair at 30 on a margin account
Romazes Nov 9, 2026
468d705
test: brokerage test harness guards a quantity update like the transa…
Romazes Nov 9, 2026
e3c8ccc
refactor: contingent set keeps one quantities view
Romazes Nov 9, 2026
6f696b7
fix: etoro model keeps an exit price on its side of the market
Romazes Nov 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion Common/Brokerages/BrokerageName.cs
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,11 @@ public enum BrokerageName
/// <summary>
/// Transaction and submit/execution rules will use Quant Bridge models
/// </summary>
QuantBridge
QuantBridge,

/// <summary>
/// Transaction and submit/execution rules will use eToro models
/// </summary>
Etoro
}
}
331 changes: 331 additions & 0 deletions Common/Brokerages/EtoroBrokerageModel.cs

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions Common/Brokerages/IBrokerageModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,9 @@ public static IBrokerageModel Create(IOrderProvider orderProvider, BrokerageName
case BrokerageName.ClearStreet:
return new ClearStreetBrokerageModel(accountType);

case BrokerageName.Etoro:
return new EtoroBrokerageModel(accountType);

default:
throw new ArgumentOutOfRangeException(nameof(brokerage), brokerage, null);
}
Expand Down Expand Up @@ -424,6 +427,9 @@ public static BrokerageName GetBrokerageName(IBrokerageModel brokerageModel)
case ClearStreetBrokerageModel:
return BrokerageName.ClearStreet;

case EtoroBrokerageModel:
return BrokerageName.Etoro;

case DefaultBrokerageModel _:
return BrokerageName.Default;

Expand Down
51 changes: 51 additions & 0 deletions Common/Messages/Messages.Brokerages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,57 @@ public static string ExtendedMarketOrderMustBeLimit(Orders.Order order)
}
}

/// <summary>
/// Provides user-facing messages for the <see cref="Brokerages.EtoroBrokerageModel"/> class and its consumers or related classes
/// </summary>
public static class EtoroBrokerageModel
{
/// <summary>
/// Returns a message explaining that a short sale needs a stop loss: a stop market order it triggers.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string ShortSaleRequiresStopLoss(Orders.Order order)
{
return Invariant($"A short sale of {order.Symbol.Value} at eToro needs a stop loss: a stop market order the short sale triggers, as a one triggers other or a bracket order. eToro closes the short position at its stop price.");
}

/// <summary>
/// Returns a message explaining that a position with leverage above 1 needs a stop loss: a stop market order the entry triggers.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string LeverageRequiresStopLoss(Orders.Order order, decimal leverage)
{
return Invariant($"A position in {order.Symbol.Value} at leverage {leverage} at eToro needs a stop loss: a stop market order the entry triggers, as a one triggers other or a bracket order. eToro closes the position at its stop price.");
}

/// <summary>
/// Returns a message explaining that a crypto stop loss has to be at least a given percent away from the price.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string CryptoStopLossTooClose(Orders.Order order, decimal price, int minimumPercent)
{
return Invariant($"The stop loss of {order.Symbol.Value} at eToro has to be at least {minimumPercent}% away from the price {price}: eToro keeps a crypto stop loss that far from the position and moves a closer one there.");
}

/// <summary>
/// Returns a message explaining that a stop loss has to be below the price of a long position and above the price of a short one.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string StopLossOnTheWrongSide(Orders.Order order, decimal stopPrice, decimal price)
{
return Invariant($"The stop loss of {order.Symbol.Value} at {stopPrice} is on the wrong side of the price {price}: eToro keeps the stop loss of a long position below the price and that of a short position above it.");
}

/// <summary>
/// Returns a message explaining that a take profit has to be above the price of a long position and below the price of a short one.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string TakeProfitOnTheWrongSide(Orders.Order order, decimal limitPrice, decimal price)
{
return Invariant($"The take profit of {order.Symbol.Value} at {limitPrice} is on the wrong side of the price {price}: eToro keeps the take profit of a long position above the price and that of a short position below it.");
}
}

/// <summary>
/// Provides user-facing messages for the <see cref="Brokerages.RBIBrokerageModel"/> class and its consumers or related classes
/// </summary>
Expand Down
59 changes: 59 additions & 0 deletions Common/Orders/Fees/EtoroFeeModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

using QuantConnect.Securities;

namespace QuantConnect.Orders.Fees
{
/// <summary>
/// Represents a fee model specific to eToro.
/// </summary>
/// <see href="https://www.etoro.com/en-us/trading/fees/"/>
/// <remarks>
/// Stock and ETF trades are commission free. Crypto trades pay 1% of the trade value on every buy and sell.
/// The stock commission on the global fees page (https://www.etoro.com/trading/fees/) depends on the country
/// of residence and the exchange; the model uses the US page, which charges nothing.
/// </remarks>
public class EtoroFeeModel : FeeModel
{
/// <summary>
/// Crypto fee rate on the trade value, charged on every buy and sell.
/// From https://www.etoro.com/en-us/trading/fees/:
/// "A standardized 1% commission is charged each time you buy or sell a cryptoasset on etoro."
/// </summary>
private const decimal _cryptoFeeRate = 0.01m;

/// <summary>
/// Gets the order fee for a given security and order.
/// </summary>
/// <param name="parameters">The parameters including the security and order details.</param>
/// <returns>A <see cref="OrderFee"/> in the quote currency of the security.</returns>
public override OrderFee GetOrderFee(OrderFeeParameters parameters)
{
var order = parameters.Order;
var security = parameters.Security;

// From https://www.etoro.com/en-us/trading/fees/: "We charge zero commissions when you buy or sell a stock."
if (security.Type != SecurityType.Crypto)
{
return OrderFee.Zero;
}

var tradeValue = security.Price * security.SymbolProperties.ContractMultiplier * order.AbsoluteQuantity;
return new OrderFee(new CashAmount(tradeValue * _cryptoFeeRate, security.QuoteCurrency.Symbol));
}
}
}
19 changes: 19 additions & 0 deletions Common/Orders/OrderContingency.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@ public class OrderContingency
[JsonIgnore]
public IReadOnlySet<OrderType> OrderTypes => _set.OrderTypes ??= _set.BuildSet(member => member.OrderType);

/// <summary>
/// The quantities of the orders in the set, one per order, signed. Allows a brokerage model to validate a single
/// order against the side and the size of each other order of the set. Only available at submission time
/// </summary>
[JsonIgnore]
public IReadOnlyList<decimal> Quantities => _set.Quantities ??= _set.BuildList(member => member.Quantity);
Comment thread
Romazes marked this conversation as resolved.
Resolved

/// <summary>
/// The links of this order to the rest of the set: the role it plays in each contingency
/// </summary>
Expand Down Expand Up @@ -513,6 +520,7 @@ private class SharedState
public HashSet<Symbol> Symbols { get; set; }
public HashSet<OrderDirection> Directions { get; set; }
public HashSet<OrderType> OrderTypes { get; set; }
public List<decimal> Quantities { get; set; }
public List<SubmitOrderRequest> Requests { get; set; }

public SharedState(int id, int count)
Expand All @@ -528,6 +536,7 @@ public void OnMembersChanged()
Symbols = null;
Directions = null;
OrderTypes = null;
Quantities = null;
Requests = null;
}

Expand All @@ -541,6 +550,16 @@ public HashSet<T> BuildSet<T>(Func<Member, T> selector)
return result;
}

public List<T> BuildList<T>(Func<Member, T> selector)
{
var result = new List<T>(Members.Count);
foreach (var member in Members)
{
result.Add(selector(member));
}
return result;
}

public List<SubmitOrderRequest> BuildRequests()
{
var result = new List<SubmitOrderRequest>(Members.Count);
Expand Down
21 changes: 21 additions & 0 deletions Launcher/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,12 @@
// Users can have more than one account
"clearstreet-account-id": "",

// eToro configuration
"etoro-api-url": "https://public-api.etoro.com",
"etoro-api-key": "",
"etoro-user-key": "",
"etoro-demo-environment": true,

// Exante trading configuration
// client-id, application-id, shared-key are required to access Exante REST API
// Exante generates them at https://exante.eu/clientsarea/dashboard/ after adding an application
Expand Down Expand Up @@ -851,6 +857,21 @@
"history-provider": [ "BrokerageHistoryProvider", "SubscriptionDataReaderHistoryProvider" ]
},

// defines the 'live-etoro' environment
"live-etoro": {
"live-mode": true,

// real brokerage implementations require the BrokerageTransactionHandler
"live-mode-brokerage": "EtoroBrokerage",
"data-queue-handler": [ "EtoroBrokerage" ],
"setup-handler": "QuantConnect.Lean.Engine.Setup.BrokerageSetupHandler",
"result-handler": "QuantConnect.Lean.Engine.Results.LiveTradingResultHandler",
"data-feed-handler": "QuantConnect.Lean.Engine.DataFeeds.LiveTradingDataFeed",
"real-time-handler": "QuantConnect.Lean.Engine.RealTime.LiveTradingRealTimeHandler",
"transaction-handler": "QuantConnect.Lean.Engine.TransactionHandlers.BrokerageTransactionHandler",
"history-provider": [ "BrokerageHistoryProvider", "SubscriptionDataReaderHistoryProvider" ]
},

// defines the 'live-dydx' environment
"live-dydx": {
"live-mode": true,
Expand Down
15 changes: 13 additions & 2 deletions Tests/Brokerages/BrokerageTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -138,14 +138,25 @@ private IBrokerage InitializeBrokerage()

/// <summary>
/// Applies the brokerage updates of the orders the way the transaction handler does: contingent orders held waiting
/// for their parent are released
/// for their parent are released, and an order the brokerage resized takes the new quantity
/// </summary>
private void HandleOrderUpdatedEvents(object _, OrderUpdateEvent orderUpdateEvent)
{
if (orderUpdateEvent.ContingencyTriggered && OrderProvider.GetOrderById(orderUpdateEvent.OrderId)?.GetContingencyLink(ContingencyRole.Child) is { } child)
var order = OrderProvider.GetOrderById(orderUpdateEvent.OrderId);
if (orderUpdateEvent.ContingencyTriggered && order?.GetContingencyLink(ContingencyRole.Child) is { } child)
{
child.Triggered = true;
}
if (orderUpdateEvent.Quantity.HasValue && order != null)
Comment thread
Romazes marked this conversation as resolved.
Resolved
{
// the transaction handler drops a zero quantity, a change of side and a quantity under what already filled
var quantity = orderUpdateEvent.Quantity.Value;
var filledQuantity = OrderProvider.GetOrderTicket(order.Id)?.QuantityFilled ?? 0;
if (quantity != 0 && quantity != order.Quantity && Math.Sign(quantity) == Math.Sign(order.Quantity) && Math.Abs(quantity) >= Math.Abs(filledQuantity))
{
order.Quantity = quantity;
}
}
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ private static IEnumerable<IBrokerageModel> NotSupportedBrokerageModels()
typeof(CharlesSchwabBrokerageModel),
typeof(TradeStationBrokerageModel),
typeof(AlpacaBrokerageModel),
typeof(EtoroBrokerageModel),
typeof(BinanceBrokerageModel),
typeof(BinanceFuturesBrokerageModel),
typeof(BinanceCoinFuturesBrokerageModel)
Expand Down
Loading
Loading