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

Added Accounts and Positions endpoints #36

Merged
merged 34 commits into from
Dec 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
f6638c4
Added Accounts and Positions endpoints
ChuchoCoder Oct 21, 2023
f78b8fe
Removed unused elements.
naicigam Dec 19, 2023
6f12e51
Removed comments.
naicigam Dec 19, 2023
b56995a
Refactored position and account tests.
naicigam Dec 19, 2023
36fbbe1
Removed comments.
naicigam Dec 19, 2023
c7ee82a
Removed unused elements.
naicigam Dec 19, 2023
a9d9bcb
Refactored position and account tests.
naicigam Dec 19, 2023
67e8bab
Merge branch 'feature/accounts-positions' of https://github.com/Chuch…
naicigam Dec 19, 2023
8ec16fe
Removed unused elements.
naicigam Dec 19, 2023
8ce8c7f
Merge branch 'feature/accounts-positions' of https://github.com/Chuch…
naicigam Dec 19, 2023
0a8aa9c
Refactored position and account tests.
naicigam Dec 19, 2023
0b253f1
Merge branch 'feature/accounts-positions' of https://github.com/Chuch…
naicigam Dec 19, 2023
c6869c8
Removed comments.
naicigam Dec 19, 2023
7a239dc
Merge branch 'feature/accounts-positions' of https://github.com/Chuch…
naicigam Dec 19, 2023
9935a90
Api.GetAccounts now returns IEnumerable.
naicigam Dec 19, 2023
41ef3fe
Tests throw exception when order is rejected.
naicigam Dec 19, 2023
710f71b
Added Instrument min and max trade volume.
naicigam Dec 20, 2023
196b373
Added Instrument min and max trade volume.
naicigam Dec 20, 2023
ef4ef62
Merge branch 'feature/accounts-positions' of https://github.com/Chuch…
naicigam Dec 20, 2023
83fa94c
Updated README.
naicigam Dec 20, 2023
4c339e2
Api.GetPositions now returns IEnumerable.
naicigam Dec 20, 2023
db4bdd9
CI build tests verbosity.
naicigam Dec 20, 2023
bdd123e
Debug statements to debug github actions.
naicigam Dec 20, 2023
5569808
Debug statements to debug github actions.
naicigam Dec 20, 2023
744c7dc
Merge branch 'feature/accounts-positions' of https://github.com/Chuch…
naicigam Dec 20, 2023
5dede00
Github action to debug test case failure.
naicigam Dec 20, 2023
9e9ea46
Merge branch 'feature/accounts-positions' of https://github.com/Chuch…
naicigam Dec 20, 2023
5f561b8
Debugging GH action failure.
naicigam Dec 20, 2023
8104da0
Debugging GH action failure.
naicigam Dec 20, 2023
c43e8f1
Debugging GH action failure.
naicigam Dec 20, 2023
a1f6aec
Debugging GH action failure.
naicigam Dec 20, 2023
720a69d
Debugging GH action failure.
naicigam Dec 20, 2023
4aaa9bf
Rename GetPositions to GetAccountPositions.
naicigam Dec 20, 2023
5fde700
Removed all debugging statements.
naicigam Dec 20, 2023
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
8 changes: 4 additions & 4 deletions .github/workflows/ci-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ jobs:
name: CI Build
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3

- name: Setup .NET
uses: actions/setup-dotnet@v1
uses: actions/setup-dotnet@v3
with:
dotnet-version: 6.0.x

Expand All @@ -35,8 +35,8 @@ jobs:
run: dotnet build Primary.sln --configuration Release --no-restore

- name: Test
run: dotnet test Primary.sln --configuration Release --no-build --verbosity normal --collect:"XPlat Code Coverage" --results-directory ./coverage

run: dotnet test Primary.sln --configuration Release -- no-build --verbosity normal --collect:"XPlat Code Coverage" --results-directory ./coverage
- name: Copy Coverage To Predictable Location
run: cp coverage/**/coverage.cobertura.xml coverage.cobertura.xml

Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,5 @@ bld/
/Primary.Tests/bin
/Primary.Tests/obj
*.user

coverage/
54 changes: 53 additions & 1 deletion Primary.Tests/AccountsTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using NUnit.Framework;
using Primary.Data.Orders;
using System.Linq;
using System.Threading.Tasks;

Expand All @@ -25,5 +26,56 @@ public async Task AccountStatementCanBeRetrieved()
Assert.That(detailedAccountReport.CurrencyBalance, Is.Not.Null.And.Not.Empty);
Assert.That(detailedAccountReport.AvailableToOperate.Cash.DetailedCash, Is.Not.Empty);
}

[Test]
public async Task AccountsCanBeRetrieved()
{
var accounts = await Api.GetAccounts();
Assert.That(accounts, Is.Not.Empty);

var account = accounts.First();
Assert.That(account.BrokerId, Is.Not.EqualTo(default));
Assert.That(account.Id, Is.Not.EqualTo(default));
Assert.That(account.Name, Is.EqualTo(Api.DemoAccount));
Assert.That(account.Status, Is.True);
}

[Test]
[Timeout(20000)]
public async Task PositionsCanBeRetrieved()
{
var marketData = await GetSomeMarketData();
var symbol = marketData.InstrumentId.Symbol;

// Take the opposite side.
var order = new Order()
{
InstrumentId = marketData.InstrumentId,
Type = Type.Market,
Side = marketData.Data.Offers?.Length > 0 ? Side.Buy : Side.Sell,
Quantity = AllInstrumentsBySymbol[symbol].MinimumTradeVolume,
};

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

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

var position = positions.FirstOrDefault(p => p.Symbol == symbol);
Assert.That(position, Is.Not.EqualTo(default));
Assert.That(position.Symbol, Is.EqualTo(symbol));

if (order.Side == Side.Buy)
{
Assert.That(position.BuySize, Is.GreaterThanOrEqualTo(order.Quantity));
Assert.That(position.OriginalBuyPrice, Is.Not.EqualTo(0));
}
else
{
Assert.That(position.SellSize, Is.GreaterThanOrEqualTo(order.Quantity));
Assert.That(position.OriginalSellPrice, Is.Not.EqualTo(0));
}
}
}
}
}
2 changes: 2 additions & 0 deletions Primary.Tests/ApiTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ public async Task AllAvailableInstrumentsCanBeRetrieved()
Assert.That(instrument.PriceConversionFactor, Is.Not.EqualTo(default));
Assert.That(instrument.CfiCode, Is.Not.Null.And.Not.Empty);
Assert.That(instrument.Type, Is.Not.EqualTo(InstrumentType.Unknown));
Assert.That(instrument.MinimumTradeVolume, Is.GreaterThanOrEqualTo(0));
Assert.That(instrument.MaximumTradeVolume, Is.GreaterThanOrEqualTo(0));
}

Assert.That(instruments.Where(i => i.MaturityDate != default), Is.Not.Empty);
Expand Down
66 changes: 63 additions & 3 deletions Primary.Tests/TestWithApi.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,71 @@
using System;
using Primary.Data;
using Primary.Data.Orders;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Primary.Tests
{
internal class TestWithApi
{
protected Api Api { get { return _lazyApi.Value; } }
protected Api Api => _lazyApi.Value;

private static readonly Lazy<Api> _lazyApi = new Lazy<Api>(() => Build.AnApi().Result);
private static readonly Lazy<Api> _lazyApi = new(() => Build.AnApi().Result);

protected async Task<MarketData> GetSomeMarketData()
{
if (AllInstrumentsBySymbol == null)
{
AllInstrumentsBySymbol = (await Api.GetAllInstruments()).ToDictionary(i => i.Symbol, i => i);
}

using var socket = Api.CreateMarketDataSocket(AllInstrumentsBySymbol.Values, new[] { Entry.Offers, Entry.Bids }, 1, 1);

MarketData retrievedData = null;
var dataSemaphore = new SemaphoreSlim(0, 1);
socket.OnData = ((_, marketData) =>
{
var instrument = AllInstrumentsBySymbol[marketData.InstrumentId.Symbol];

if (instrument.Type == InstrumentType.Equity &&
(
(marketData.Data.Offers != null &&
marketData.Data.Offers[0].Size > instrument.MinimumTradeVolume * 2)
||
(marketData.Data.Bids != null &&
marketData.Data.Bids[0].Size > instrument.MinimumTradeVolume * 2)
)
)
{
retrievedData = marketData;
dataSemaphore.Release();
}
});

await socket.Start();
await dataSemaphore.WaitAsync();

return retrievedData;
}

protected async Task<OrderStatus> WaitForOrderToComplete(OrderId orderId)
{
var orderStatus = await Api.GetOrderStatus(orderId);
while (orderStatus.Status != Status.Rejected && orderStatus.Status != Status.Filled)
{
Thread.Sleep(200);
orderStatus = await Api.GetOrderStatus(orderId);
}

if (orderStatus.Status == Status.Rejected)
{
throw new InvalidOperationException($"{orderStatus.StatusText} / {orderStatus.InstrumentId.Symbol} / {orderStatus.Side}");
}
return orderStatus;
}

protected Dictionary<string, Instrument> AllInstrumentsBySymbol;
}
}
1 change: 1 addition & 0 deletions Primary.sln
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3559615F-B6AA-46C5-882E-0DC3A0BB950B}"
ProjectSection(SolutionItems) = preProject
.gitignore = .gitignore
global,json.txt = global,json.txt
README.md = README.md
EndProjectSection
EndProject
Expand Down
61 changes: 61 additions & 0 deletions Primary/Api.cs
Original file line number Diff line number Diff line change
Expand Up @@ -130,13 +130,13 @@
private class GetTradesResponse
{
[JsonProperty("status")]
public string Status;

Check warning on line 133 in Primary/Api.cs

View workflow job for this annotation

GitHub Actions / CI Build

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

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

Check warning on line 136 in Primary/Api.cs

View workflow job for this annotation

GitHub Actions / CI Build

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

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

Check warning on line 139 in Primary/Api.cs

View workflow job for this annotation

GitHub Actions / CI Build

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

[JsonProperty("trades")]
public List<Trade> Trades { get; set; }
Expand Down Expand Up @@ -351,7 +351,7 @@
private struct StatusResponse
{
[JsonProperty("status")]
public string Status;

Check warning on line 354 in Primary/Api.cs

View workflow job for this annotation

GitHub Actions / CI Build

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

[JsonProperty("message")]
public string Message;
Expand Down Expand Up @@ -385,13 +385,13 @@
private struct OrderIdResponse
{
[JsonProperty("status")]
public string Status;

Check warning on line 388 in Primary/Api.cs

View workflow job for this annotation

GitHub Actions / CI Build

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

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

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

Check warning on line 394 in Primary/Api.cs

View workflow job for this annotation

GitHub Actions / CI Build

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

public struct Id
{
Expand All @@ -403,19 +403,19 @@
}

[JsonProperty("order")]
public Id Order;

Check warning on line 406 in Primary/Api.cs

View workflow job for this annotation

GitHub Actions / CI Build

Field 'Api.OrderIdResponse.Order' is never assigned to, and will always have its default value
}

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

Check warning on line 412 in Primary/Api.cs

View workflow job for this annotation

GitHub Actions / CI Build

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

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

Check warning on line 415 in Primary/Api.cs

View workflow job for this annotation

GitHub Actions / CI Build

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

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

Check warning on line 418 in Primary/Api.cs

View workflow job for this annotation

GitHub Actions / CI Build

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

[JsonProperty("order")]
public OrderStatus Order { get; set; }
Expand All @@ -425,6 +425,35 @@

#region Accounts

public async Task<IEnumerable<Account>> GetAccounts()
{
var builder = new UriBuilder(BaseUri + "rest/accounts");

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

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

public class AccountsResponse
{
[JsonProperty("status")]
public string Status;

[JsonProperty("message")]
public string Message { get; set; }

[JsonProperty("description")]
public string Description { get; set; }

[JsonProperty("accounts")]
public List<Account> Accounts { get; set; }
}

public async Task<AccountStatement> GetAccountStatement(string accountId)
{
var uri = new Uri(BaseUri, "/rest/risk/accountReport/" + accountId);
Expand Down Expand Up @@ -457,6 +486,38 @@

#endregion

#region Positions

public async Task<IEnumerable<Position>> GetAccountPositions(string accountName)
{
var builder = new UriBuilder(BaseUri + $"rest/risk/position/getPositions/{accountName}");
var jsonResponse = await HttpClient.GetStringAsync(builder.Uri);

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

public class PositionsResponse
{
[JsonProperty("status")]
public string Status;

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

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

[JsonProperty("positions")]
public List<Position> Positions { get; set; }
}

#endregion

#region Constants

private static class Status
Expand Down
23 changes: 23 additions & 0 deletions Primary/Data/Account.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Primary.Data;

public class Account
{
[JsonProperty("id")]
public int Id { get; set; }

[JsonProperty("name")]
public string Name { get; set; }

[JsonProperty("brokerId")]
public int BrokerId { get; set; }

[JsonProperty("status")]
public bool Status { get; set; }
}
54 changes: 28 additions & 26 deletions Primary/Data/Instrument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ public class InstrumentId
#region JSON serialization

[JsonProperty("instrumentId.marketId")]
protected string NestedMarket { get { return Market; } set { Market = value; } }
protected string NestedMarket { get => Market; set => Market = value; }

[JsonProperty("instrumentId.symbol")]
protected string NestedSymbol { get { return Symbol; } set { Symbol = value; } }
protected string NestedSymbol { get => Symbol; set => Symbol = value; }

/// <summary>This is used for serialization purposes and should not be called.</summary>
public bool ShouldSerializeNestedMarket() { return false; }
Expand Down Expand Up @@ -61,31 +61,33 @@ public class Instrument : InstrumentId
public string CfiCode { get; set; }

/// <summary>Instrument type from CFI code.</summary>
public InstrumentType Type
public InstrumentType Type => CfiCode switch
{
get
{
return CfiCode switch
{
"ESXXXX" => InstrumentType.Equity,
"DBXXXX" => InstrumentType.Bond,
"OCASPS" => InstrumentType.EquityCallOption,
"OPASPS" => InstrumentType.EquityPutOption,
"FXXXSX" => InstrumentType.Future,
"OPAFXS" => InstrumentType.FuturePutOption,
"OCAFXS" => InstrumentType.FutureCallOption,
"EMXXXX" => InstrumentType.Cedear,
"DBXXFR" => InstrumentType.Obligation,
"MRIXXX" => InstrumentType.Index,
"FXXXXX" => InstrumentType.Future,
"RPXXXX" => InstrumentType.Caucion,
"MXXXXX" => InstrumentType.Miscellaneous,
"LRSTXH" => InstrumentType.Miscellaneous,
"DYXTXR" => InstrumentType.TreasureNotes,
_ => InstrumentType.Unknown
};
}
}
"ESXXXX" => InstrumentType.Equity,
"DBXXXX" => InstrumentType.Bond,
"OCASPS" => InstrumentType.EquityCallOption,
"OPASPS" => InstrumentType.EquityPutOption,
"FXXXSX" => InstrumentType.Future,
"OPAFXS" => InstrumentType.FuturePutOption,
"OCAFXS" => InstrumentType.FutureCallOption,
"EMXXXX" => InstrumentType.Cedear,
"DBXXFR" => InstrumentType.Obligation,
"MRIXXX" => InstrumentType.Index,
"FXXXXX" => InstrumentType.Future,
"RPXXXX" => InstrumentType.Caucion,
"MXXXXX" => InstrumentType.Miscellaneous,
"LRSTXH" => InstrumentType.Miscellaneous,
"DYXTXR" => InstrumentType.TreasureNotes,
_ => InstrumentType.Unknown
};

/// <summary>Minimum volume that can be traded for this instrument.</summary>
[JsonProperty("minTradeVol")]
public uint MinimumTradeVolume { get; set; }

/// <summary>Maximum volume that can be traded for this instrument.</summary>
[JsonProperty("maxTradeVol")]
public uint MaximumTradeVolume { get; set; }
}

public enum InstrumentType
Expand Down
Loading
Loading