Skip to content

Commit

Permalink
Support multi-leg orders for complex option strategies
Browse files Browse the repository at this point in the history
Merge pull request #783 from the `alpacahq/feature/order-multi-legs` branch
  • Loading branch information
OlegRa authored Jan 29, 2025
2 parents 3db54f8 + 384ef85 commit 3b62263
Show file tree
Hide file tree
Showing 18 changed files with 801 additions and 35 deletions.
6 changes: 3 additions & 3 deletions Alpaca.Markets.Extensions/Alpaca.Markets.Extensions.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
</PropertyGroup>

<PropertyGroup>
<AssemblyVersion>8.0.0.0</AssemblyVersion>
<FileVersion>8.0.0.0</FileVersion>
<Version>8.0.0-alpha1</Version>
<AssemblyVersion>8.0.0.1</AssemblyVersion>
<FileVersion>8.0.0.1</FileVersion>
<Version>8.0.0-beta1</Version>
</PropertyGroup>

<PropertyGroup>
Expand Down
50 changes: 50 additions & 0 deletions Alpaca.Markets.Tests/OrderTypeTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,48 @@ public void BracketOrderCreationWorks()
.StopLoss(stopLossStopPrice, stopLossLimitPrice));
}

[Fact]
public void MultiLegOrderCreationWorks()
{
const Decimal limitPrice = 100M;
const Decimal ratioQuantity = 0.25M;

var lfi = getOrderLegCreationInfo()
.Select(info => info.Intent.Leg(Stock, ratioQuantity, info.Side))
.ToList();
var lfs = getOrderLegCreationInfo()
.Select(info => info.Side.Leg(Stock, ratioQuantity, info.Intent))
.ToList();

assertOrdersAreEqual(
MultiLegOrder.Market(Quantity, lfi[0], lfi[1]),
MultiLegOrder.Market(Quantity, lfs[0], lfs[1]));
assertOrdersAreEqual(
MultiLegOrder.Market(Quantity, lfi[0], lfi[1], lfi[2]),
MultiLegOrder.Market(Quantity, lfs[0], lfs[1], lfs[2]));
assertOrdersAreEqual(
MultiLegOrder.Market(Quantity, lfi[0], lfi[1], lfi[2], lfi[3]),
MultiLegOrder.Market(Quantity, lfs[0], lfs[1], lfs[2], lfs[3]));

assertOrdersAreEqual(
MultiLegOrder.Limit(Quantity, limitPrice, lfi[0], lfi[1]),
MultiLegOrder.Limit(Quantity, limitPrice, lfs[0], lfs[1]));
assertOrdersAreEqual(
MultiLegOrder.Limit(Quantity, limitPrice, lfi[0], lfi[1], lfi[2]),
MultiLegOrder.Limit(Quantity, limitPrice, lfs[0], lfs[1], lfs[2]));
assertOrdersAreEqual(
MultiLegOrder.Limit(Quantity, limitPrice, lfi[0], lfi[1], lfi[2], lfi[3]),
MultiLegOrder.Limit(Quantity, limitPrice, lfs[0], lfs[1], lfs[2], lfs[3]));
}

private static IEnumerable<(PositionIntent Intent, OrderSide Side)> getOrderLegCreationInfo()
{
yield return (PositionIntent.BuyToClose, OrderSide.Buy);
yield return (PositionIntent.SellToClose, OrderSide.Sell);
yield return (PositionIntent.BuyToOpen, OrderSide.Buy);
yield return (PositionIntent.SellToOpen, OrderSide.Sell);
}

private static void assertOrdersAreEqual(
OrderBase lhs,
OrderBase rhs)
Expand Down Expand Up @@ -227,6 +269,14 @@ private static void assertOrderBasePropertiesAreEqual(
Assert.Equal(lhs.Type, rhs.Type);
}

private static void assertOrdersAreEqual(
MultiLegOrder lhs,
MultiLegOrder rhs)
{
assertOrderBasePropertiesAreEqual(lhs, rhs);
assertJsonSerializedOrdersAreEqual(lhs, rhs);
}

private static void assertJsonSerializedOrdersAreEqual(
OrderBase lhs,
OrderBase rhs) =>
Expand Down
7 changes: 4 additions & 3 deletions Alpaca.Markets/Alpaca.Markets.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@
</PropertyGroup>

<PropertyGroup>
<AssemblyVersion>8.0.0.0</AssemblyVersion>
<FileVersion>8.0.0.0</FileVersion>
<Version>8.0.0-alpha1</Version>
<AssemblyVersion>8.0.0.1</AssemblyVersion>
<FileVersion>8.0.0.1</FileVersion>
<Version>8.0.0-beta1</Version>
</PropertyGroup>

<PropertyGroup>
<PackageReleaseNotes>
- Obsolete members cleanup - remove fully obsolete and convert warnings into errors.
- Initial support for the options multi-legs orders were added in experimental mode.
</PackageReleaseNotes>
<Description>C# SDK for Alpaca Trade API https://docs.alpaca.markets/</Description>
<RepositoryUrl>https://github.com/alpacahq/alpaca-trade-api-csharp</RepositoryUrl>
Expand Down
175 changes: 175 additions & 0 deletions Alpaca.Markets/CompatibilitySuppressions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,27 @@
<Right>lib/net462/Alpaca.Markets.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Alpaca.Markets.IOrder.get_AssetClass</Target>
<Left>lib/net462/Alpaca.Markets.dll</Left>
<Right>lib/net462/Alpaca.Markets.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Alpaca.Markets.IOrder.get_AssetId</Target>
<Left>lib/net462/Alpaca.Markets.dll</Left>
<Right>lib/net462/Alpaca.Markets.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Alpaca.Markets.IOrder.get_OrderSide</Target>
<Left>lib/net462/Alpaca.Markets.dll</Left>
<Right>lib/net462/Alpaca.Markets.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Alpaca.Markets.LatestDataListRequest.#ctor(System.Collections.Generic.IEnumerable{System.String},Alpaca.Markets.CryptoExchange)</Target>
Expand All @@ -568,6 +589,13 @@
<Right>lib/net462/Alpaca.Markets.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Alpaca.Markets.NewOrderRequest.get_Side</Target>
<Left>lib/net462/Alpaca.Markets.dll</Left>
<Right>lib/net462/Alpaca.Markets.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Alpaca.Markets.PortfolioHistoryRequest.get_TimeInterval</Target>
Expand Down Expand Up @@ -848,6 +876,27 @@
<Right>lib/netstandard2.1/Alpaca.Markets.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Alpaca.Markets.IOrder.get_AssetClass</Target>
<Left>lib/net6.0/Alpaca.Markets.dll</Left>
<Right>lib/netstandard2.1/Alpaca.Markets.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Alpaca.Markets.IOrder.get_AssetId</Target>
<Left>lib/net6.0/Alpaca.Markets.dll</Left>
<Right>lib/netstandard2.1/Alpaca.Markets.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Alpaca.Markets.IOrder.get_OrderSide</Target>
<Left>lib/net6.0/Alpaca.Markets.dll</Left>
<Right>lib/netstandard2.1/Alpaca.Markets.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Alpaca.Markets.LatestDataListRequest.#ctor(System.Collections.Generic.IEnumerable{System.String},Alpaca.Markets.CryptoExchange)</Target>
Expand All @@ -862,6 +911,13 @@
<Right>lib/netstandard2.1/Alpaca.Markets.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Alpaca.Markets.NewOrderRequest.get_Side</Target>
<Left>lib/net6.0/Alpaca.Markets.dll</Left>
<Right>lib/netstandard2.1/Alpaca.Markets.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Alpaca.Markets.PortfolioHistoryRequest.get_TimeInterval</Target>
Expand Down Expand Up @@ -1142,6 +1198,27 @@
<Right>lib/netstandard2.0/Alpaca.Markets.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Alpaca.Markets.IOrder.get_AssetClass</Target>
<Left>lib/netstandard2.0/Alpaca.Markets.dll</Left>
<Right>lib/netstandard2.0/Alpaca.Markets.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Alpaca.Markets.IOrder.get_AssetId</Target>
<Left>lib/netstandard2.0/Alpaca.Markets.dll</Left>
<Right>lib/netstandard2.0/Alpaca.Markets.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Alpaca.Markets.IOrder.get_OrderSide</Target>
<Left>lib/netstandard2.0/Alpaca.Markets.dll</Left>
<Right>lib/netstandard2.0/Alpaca.Markets.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Alpaca.Markets.LatestDataListRequest.#ctor(System.Collections.Generic.IEnumerable{System.String},Alpaca.Markets.CryptoExchange)</Target>
Expand All @@ -1156,6 +1233,13 @@
<Right>lib/netstandard2.0/Alpaca.Markets.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Alpaca.Markets.NewOrderRequest.get_Side</Target>
<Left>lib/netstandard2.0/Alpaca.Markets.dll</Left>
<Right>lib/netstandard2.0/Alpaca.Markets.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Alpaca.Markets.PortfolioHistoryRequest.get_TimeInterval</Target>
Expand Down Expand Up @@ -1436,6 +1520,27 @@
<Right>lib/netstandard2.1/Alpaca.Markets.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Alpaca.Markets.IOrder.get_AssetClass</Target>
<Left>lib/netstandard2.1/Alpaca.Markets.dll</Left>
<Right>lib/netstandard2.1/Alpaca.Markets.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Alpaca.Markets.IOrder.get_AssetId</Target>
<Left>lib/netstandard2.1/Alpaca.Markets.dll</Left>
<Right>lib/netstandard2.1/Alpaca.Markets.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Alpaca.Markets.IOrder.get_OrderSide</Target>
<Left>lib/netstandard2.1/Alpaca.Markets.dll</Left>
<Right>lib/netstandard2.1/Alpaca.Markets.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Alpaca.Markets.LatestDataListRequest.#ctor(System.Collections.Generic.IEnumerable{System.String},Alpaca.Markets.CryptoExchange)</Target>
Expand All @@ -1450,13 +1555,83 @@
<Right>lib/netstandard2.1/Alpaca.Markets.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Alpaca.Markets.NewOrderRequest.get_Side</Target>
<Left>lib/netstandard2.1/Alpaca.Markets.dll</Left>
<Right>lib/netstandard2.1/Alpaca.Markets.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Alpaca.Markets.PortfolioHistoryRequest.get_TimeInterval</Target>
<Left>lib/netstandard2.1/Alpaca.Markets.dll</Left>
<Right>lib/netstandard2.1/Alpaca.Markets.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Alpaca.Markets.IOrder.AssetClass</Target>
<Left>lib/net6.0/Alpaca.Markets.dll</Left>
<Right>lib/netstandard2.1/Alpaca.Markets.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Alpaca.Markets.IOrder.AssetId</Target>
<Left>lib/net6.0/Alpaca.Markets.dll</Left>
<Right>lib/netstandard2.1/Alpaca.Markets.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Alpaca.Markets.IOrder.OrderSide</Target>
<Left>lib/net6.0/Alpaca.Markets.dll</Left>
<Right>lib/netstandard2.1/Alpaca.Markets.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Alpaca.Markets.IOrder.AssetClass</Target>
<Left>lib/netstandard2.0/Alpaca.Markets.dll</Left>
<Right>lib/netstandard2.0/Alpaca.Markets.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Alpaca.Markets.IOrder.AssetId</Target>
<Left>lib/netstandard2.0/Alpaca.Markets.dll</Left>
<Right>lib/netstandard2.0/Alpaca.Markets.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Alpaca.Markets.IOrder.OrderSide</Target>
<Left>lib/netstandard2.0/Alpaca.Markets.dll</Left>
<Right>lib/netstandard2.0/Alpaca.Markets.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Alpaca.Markets.IOrder.AssetClass</Target>
<Left>lib/netstandard2.1/Alpaca.Markets.dll</Left>
<Right>lib/netstandard2.1/Alpaca.Markets.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Alpaca.Markets.IOrder.AssetId</Target>
<Left>lib/netstandard2.1/Alpaca.Markets.dll</Left>
<Right>lib/netstandard2.1/Alpaca.Markets.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Alpaca.Markets.IOrder.OrderSide</Target>
<Left>lib/netstandard2.1/Alpaca.Markets.dll</Left>
<Right>lib/netstandard2.1/Alpaca.Markets.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0008</DiagnosticId>
<Target>T:Alpaca.Markets.AccountActivityType</Target>
Expand Down
9 changes: 8 additions & 1 deletion Alpaca.Markets/Enums/OrderClass.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ namespace Alpaca.Markets;
/// Order class for advanced orders in the Alpaca REST API.
/// </summary>
[JsonConverter(typeof(StringEnumConverter))]
[SuppressMessage("ReSharper", "StringLiteralTypo")]
public enum OrderClass
{
/// <summary>
Expand All @@ -29,5 +30,11 @@ public enum OrderClass
/// One Triggers Other order
/// </summary>
[EnumMember(Value = "oto")]
OneTriggersOther
OneTriggersOther,

/// <summary>
/// Multi-leg options order
/// </summary>
[EnumMember(Value = "mleg")]
MultiLegOptions
}
22 changes: 22 additions & 0 deletions Alpaca.Markets/Helpers/JsonNewOrderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,20 @@ public static JsonNewOrder WithoutLimitPrice(
return order;
}

public static JsonNewOrder WithoutOrderSide(
this JsonNewOrder order)
{
order.OrderSide = null;
return order;
}

public static JsonNewOrder WithoutSymbol(
this JsonNewOrder order)
{
order.Symbol = null;
return order;
}

public static JsonNewOrder WithStopPrice(
this JsonNewOrder order,
Decimal stopPrice)
Expand Down Expand Up @@ -70,4 +84,12 @@ public static JsonNewOrder WithStopLoss(
};
return order;
}

public static JsonNewOrder WithOrderLegs(
this JsonNewOrder order,
IEnumerable<JsonOrderLeg> legs)
{
(order.Legs ??= []).AddRange(legs);
return order;
}
}
4 changes: 4 additions & 0 deletions Alpaca.Markets/Helpers/Validation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ public static TRequest Validate<TRequest>(
return request;
}

public static IEnumerable<RequestValidationException?> GetExceptions<T>(
this IEnumerable<T> items) =>
items.OfType<IRequest>().SelectMany(request => request.GetExceptions());

public static RequestValidationException? TryValidateSymbolName(
this String symbolName,
[CallerArgumentExpression(nameof(symbolName))] String propertyName = "") =>
Expand Down
Loading

0 comments on commit 3b62263

Please sign in to comment.