Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/ticks #40

Merged
merged 15 commits into from
Jun 21, 2024
18 changes: 12 additions & 6 deletions Primary.Tests/AccountsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public async Task AccountsCanBeRetrieved()
[Timeout(20000)]
public async Task PositionsCanBeRetrieved()
{
var instrument = (await AnotherApi.GetAllInstruments()).First(i => i.Type == InstrumentType.Equity);
var instrument = await RandomInstrument(AnotherApi);
var symbol = instrument.Symbol;

// Generate liquidity
Expand All @@ -54,7 +54,7 @@ public async Task PositionsCanBeRetrieved()
InstrumentId = instrument,
Type = Type.Limit,
Price = instrument.MinimumTradePrice + 10,
Side = Side.Buy,
Side = Side.Sell,
Quantity = instrument.MinimumTradeVolume
};

Expand All @@ -65,16 +65,15 @@ public async Task PositionsCanBeRetrieved()
var order = new Order()
{
InstrumentId = instrument,
Type = Type.Limit,
Price = instrument.MinimumTradePrice,
Side = Side.Sell,
Type = Type.Market,
Side = Side.Buy,
Quantity = instrument.MinimumTradeVolume
};

orderId = await Api.SubmitOrder(ApiAccount, order);
await WaitForOrderToComplete(Api, orderId);

var positions = await Api.GetAccountPositions(Api.DemoAccount);
var positions = await Api.GetAccountPositions(ApiAccount);
Assert.That(positions, Is.Not.Null);

var position = positions.FirstOrDefault(p => p.Symbol == symbol);
Expand All @@ -92,5 +91,12 @@ public async Task PositionsCanBeRetrieved()
Assert.That(position.OriginalSellPrice, Is.Not.EqualTo(0));
}
}

private static async Task<Instrument> RandomInstrument(Api api)
{
var instruments = (await api.GetAllInstruments()).Where(i => i.Type == InstrumentType.Equity);
return instruments.ElementAt(_random.Next(0, instruments.Count()));
}
private static readonly System.Random _random = new();
}
}
22 changes: 10 additions & 12 deletions Primary.Tests/Builders/OrderBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using Primary.Data;
using Primary.Data.Orders;
using System;
using System.Collections.Generic;
using System.Linq;

namespace Primary.Tests.Builders
Expand All @@ -16,30 +16,28 @@ public OrderBuilder(Api api)

private Order Build()
{
var instrumentId = new InstrumentId()
if (_instruments == null)
{
Market = "ROFX",
Symbol = Tests.Build.DollarFutureSymbol()
};

// Get a valid price
var today = DateTime.Today;
var prices = _api.GetHistoricalTrades(instrumentId, today.AddDays(-5), today).Result;
_instruments = _api.GetAllInstruments().Result;
}

var instrument = _instruments.First(i => i.Symbol.Contains("GGAL"));
return new Order
{
InstrumentId = instrumentId,
InstrumentId = instrument,
Expiration = Expiration.Day,
Type = Data.Orders.Type.Limit,
Type = Type.Limit,
Side = Side.Buy,
Quantity = 1,
Price = prices.Last().Price - 1m
Price = instrument.MinimumTradePrice
};
}

public static implicit operator Order(OrderBuilder builder)
{
return builder.Build();
}

private IEnumerable<Instrument> _instruments;
}
}
33 changes: 32 additions & 1 deletion Primary.Tests/OrdersTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,37 @@ public async Task OrdersCanBeEnteredAndRetrieved()
Assert.That(retrievedOrder.Side, Is.EqualTo(order.Side));
}

[Test]
public async Task ActiveOrdersCanBeRetrieved()
{
// Submit some orders
Order order = Build.AnOrder(Api);
var orderId = await Api.SubmitOrder(Api.DemoAccount, order);

Order anotherOrder = Build.AnOrder(Api);
var anotherOrderId = await Api.SubmitOrder(Api.DemoAccount, anotherOrder);

Order otherToBeCancelled = Build.AnOrder(Api);
var orderToBeCancelledId = await Api.SubmitOrder(Api.DemoAccount, otherToBeCancelled);
await Api.CancelOrder(orderToBeCancelledId);

// Retrieve active orders
var activeOrderStatuses = await Api.GetActiveOrderStatuses(Api.DemoAccount);
Assert.That(activeOrderStatuses, Has.One.Matches<OrderStatus>(o => o.ClientOrderId == orderId.ClientOrderId));
Assert.That(activeOrderStatuses, Has.One.Matches<OrderStatus>(o => o.ClientOrderId == anotherOrderId.ClientOrderId));
Assert.That(activeOrderStatuses, Has.None.Matches<OrderStatus>(o => o.ClientOrderId == orderToBeCancelledId.ClientOrderId));
}

[Test]
public async Task OrderSizeAndPriceCanBeUpdated()
{
// Submit an order
Order order = Build.AnOrder(Api);
var orderId = await Api.SubmitOrder(Api.DemoAccount, order);

var orderStatus = await Api.GetOrderStatus(orderId);
Assert.That(orderStatus.Quantity, Is.EqualTo(order.Quantity));
Assert.That(orderStatus.Price, Is.EqualTo(order.Price));

var anotherQuantity = order.Quantity + 1;
var anotherPrice = order.Price + 1;
Expand Down Expand Up @@ -97,7 +121,7 @@ public void GettingAnOrderWithInvalidInformationGeneratesAnException()
};

var exception = Assert.ThrowsAsync<Exception>(async () => await Api.GetOrderStatus(invalidOrderId));
Assert.That(exception.Message, Does.Contain(invalidOrderId.ClientOrderId.ToString()));
Assert.That(exception.Message, Does.Contain(invalidOrderId.ClientOrderId));
Assert.That(exception.Message, Does.Contain(invalidOrderId.Proprietary));
}

Expand All @@ -118,5 +142,12 @@ public void CancellingAnOrderWithInvalidInformationGeneratesAnException()
var exception = Assert.ThrowsAsync<Exception>(async () => await Api.CancelOrder(order));
Assert.That(exception.Message, Does.Contain(order.ClientOrderId));
}

[Test]
public void GettingActiveOrdersWithInvalidAccountGeneratesAnException()
{
const string invalidAccountId = "invalid_account_id";
Assert.ThrowsAsync<Exception>(async () => await Api.GetActiveOrderStatuses(invalidAccountId));
}
}
}
8 changes: 4 additions & 4 deletions Primary.Tests/Primary.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1">
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.1.0">
<PackageReference Include="coverlet.collector" Version="6.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
</ItemGroup>

<ItemGroup>
Expand Down
88 changes: 63 additions & 25 deletions Primary/Api.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@
public class Api
{
/// <summary>This is the default production endpoint.</summary>
public static Uri ProductionEndpoint => new Uri("https://api.primary.com.ar");
public static Uri ProductionEndpoint => new("https://api.primary.com.ar");

/// <summary>This is the default demo endpoint.</summary>
/// <remarks>You can get a demo username at https://remarkets.primary.ventures.</remarks>
public static Uri DemoEndpoint => new Uri("https://api.remarkets.primary.com.ar");
public static Uri DemoEndpoint => new("https://api.remarkets.primary.com.ar");

/// <summary>
/// Build a new API object.
Expand Down Expand Up @@ -287,7 +287,7 @@
Accounts = accounts.Select(a => new OrderStatus.AccountId() { Id = a }).ToArray()
};

return new OrderDataWebSocket(this, request, cancellationToken);
return new OrderDataWebSocket(this, request, cancellationToken, null, _loggerFactory);
}

#endregion
Expand Down Expand Up @@ -317,8 +317,8 @@
query["side"] = order.Side.ToApiString();
query["timeInForce"] = order.Expiration.ToApiString();
query["account"] = account;
query["cancelPrevious"] = order.CancelPrevious.ToString(CultureInfo.InvariantCulture);
query["iceberg"] = order.Iceberg.ToString(CultureInfo.InvariantCulture);
query["cancelPrevious"] = JsonConvert.SerializeObject(order.CancelPrevious);
query["iceberg"] = JsonConvert.SerializeObject(order.Iceberg);

if (order.Expiration == Expiration.GoodTillDate)
{
Expand Down Expand Up @@ -408,32 +408,12 @@
};
}

private struct StatusResponse
{
[JsonProperty("status")]
public string Status;

[JsonProperty("message")]
public string Message;

[JsonProperty("description")]
public string Description;

public StatusResponse()
{
Status = null;
Message = null;
Description = null;
}
}

/// <summary>
/// Cancel an order.
/// </summary>
/// <param name="orderId">Order identifier to cancel.</param>
public async Task CancelOrder(OrderId orderId)
{

var builder = new UriBuilder(BaseUri + "/rest/order/cancelById");
var query = HttpUtility.ParseQueryString(builder.Query);
query["clOrdId"] = orderId.ClientOrderId;
Expand All @@ -449,6 +429,64 @@
}
}

/// <summary>
/// Get all the active orders for a specific account.
/// </summary>
/// <param name="accountId">Account to get orders from.</param>
public async Task<IEnumerable<OrderStatus>> GetActiveOrderStatuses(string accountId)
{
var builder = new UriBuilder(BaseUri + "/rest/order/actives");
var query = HttpUtility.ParseQueryString(builder.Query);
query["accountId"] = accountId;
builder.Query = query.ToString();

var jsonResponse = await HttpClient.GetStringAsync(builder.Uri);

var response = JsonConvert.DeserializeObject<OrdersStatusResponse>(jsonResponse);
if (response.Status == Status.Error)
{
throw new Exception($"{response.Message})");
}
return response.Orders;
}

private struct OrdersStatusResponse
{
[JsonProperty("status")]
public string Status;

[JsonProperty("message")]
public string Message;

Check warning on line 459 in Primary/Api.cs

View workflow job for this annotation

GitHub Actions / CI Build

Field 'Api.OrdersStatusResponse.Message' is never assigned to, and will always have its default value null

Check warning on line 459 in Primary/Api.cs

View workflow job for this annotation

GitHub Actions / CI Build

Field 'Api.OrdersStatusResponse.Message' is never assigned to, and will always have its default value null

[JsonProperty("orders")]
public IEnumerable<OrderStatus> Orders;

public OrdersStatusResponse()
{
Status = null;
Orders = null;
}
}

private struct StatusResponse
{
[JsonProperty("status")]
public string Status;

[JsonProperty("message")]
public string Message;

[JsonProperty("description")]
public string Description;

public StatusResponse()
{
Status = null;
Message = null;
Description = null;
}
}

private struct OrderIdResponse
{
[JsonProperty("status")]
Expand Down
19 changes: 18 additions & 1 deletion Primary/Data/Instrument.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Newtonsoft.Json;
using Primary.Serialization;
using System;
using System.Collections.Generic;

namespace Primary.Data
{
Expand Down Expand Up @@ -36,7 +37,7 @@ public class InstrumentId

public class Instrument : InstrumentId
{
/// <summary>Dezscription of the instrument.</summary>
/// <summary>Description of the instrument.</summary>
[JsonProperty("securityDescription")]
public string Description { get; set; }

Expand Down Expand Up @@ -95,6 +96,22 @@ public class Instrument : InstrumentId
/// <summary>Highest price in which it can be traded.</summary>
[JsonProperty("highLimitPrice")]
public decimal MaximumTradePrice { get; set; }

public class TickPriceRange
{
[JsonProperty("lowerLimit")]
public decimal LowerLimit { get; set; }

[JsonProperty("upperLimit")]
public decimal? UpperLimit { get; set; }

[JsonProperty("tick")]
public decimal Tick { get; set; }
}

/// <summary>Dynamic price ticks of each contract.</summary>
[JsonProperty("tickPriceRanges")]
public Dictionary<string, TickPriceRange> TickPriceRanges { get; set; }
}

public enum InstrumentType
Expand Down
10 changes: 9 additions & 1 deletion Primary/Data/Orders/Enums.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ public enum Expiration
[JsonConverter(typeof(StatusJsonSerializer))]
public enum Status
{
/// <summary>The order does not have a status yet.</summary>
NotSet,

/// <summary>The order was successfully submitted.</summary>
New,

Expand All @@ -66,7 +69,10 @@ public enum Status
PartiallyFilled,

/// <summary>The order was filled.</summary>
Filled
Filled,

/// <summary>The order expired.</summary>
Expired
}

#region String serialization
Expand Down Expand Up @@ -189,6 +195,7 @@ public static string ToApiString(this Status value)
Status.PendingReplace => "PENDING_REPLACE",
Status.PartiallyFilled => "PARTIALLY_FILLED",
Status.Filled => "FILLED",
Status.Expired => "EXPIRED",
_ => throw new InvalidEnumStringException(value.ToString()),
};
}
Expand All @@ -205,6 +212,7 @@ public static Status StatusFromApiString(string value)
"PENDING_REPLACE" => Status.PendingReplace,
"PARTIALLY_FILLED" => Status.PartiallyFilled,
"FILLED" => Status.Filled,
"EXPIRED" => Status.Expired,
_ => throw new InvalidEnumStringException(value),
};
}
Expand Down
Loading
Loading