Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 12 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -131,4 +131,15 @@ $RECYCLE.BIN/
# Mac desktop service store files
.DS_Store

_NCrunch*
_NCrunch*
/Bots/AleixBot/.vs
/Bots/ExampleTraderBot/.vs/ExampleTraderBot
/Source/.vs
/Build/NasdaqTrader.Bot.Core.dll
/Build/NasdaqTrader.CLI.dll
/Build/NasdaqTrader.CLI.exe
/Build/NasdaqTraderSystem.Core.dll
/Build/NasdaqTraderSystem.Html.dll
/Bots/AleixBot.Tests/AleixBot.Tests.csproj
/Bots/AleixBot.Tests/ListingPickerTests.cs
/Bots/AleixBot/Properties/launchSettings.json
70 changes: 69 additions & 1 deletion Bots/AleixBot/AleixBot.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,86 @@
using NasdaqTrader.Bot.Core;
using System.Diagnostics;
using System.Diagnostics.Metrics;


namespace AleixBot;

public class AleixBot : ITraderBot
{
public string CompanyName => "Beginner Investments";
public static ListingPicker _listingPicker = null;

Check warning on line 11 in Bots/AleixBot/AleixBot.cs

View workflow job for this annotation

GitHub Actions / build

Cannot convert null literal to non-nullable reference type.
private static int _counter = 0;
private const int WINDOWSIZE = 3;

public async Task DoTurn(ITraderSystemContext systemContext)

Check warning on line 15 in Bots/AleixBot/AleixBot.cs

View workflow job for this annotation

GitHub Actions / build

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 15 in Bots/AleixBot/AleixBot.cs

View workflow job for this annotation

GitHub Actions / build

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.
{
var listings = systemContext.GetListings();
var cash = systemContext.GetCurrentCash(this);
var currentDate = systemContext.CurrentDate;
var tradesLeft = systemContext.GetTradesLeftForToday(this);

systemContext.BuyStock(this, listings[0], 1);
if (_listingPicker == null)
{
InitializeListingPicker(listings, WINDOWSIZE);
}

if (_counter % WINDOWSIZE == 0 || _counter == 0)
{
//TODO implement while loop for trading
if (tradesLeft <= 0)
{
_counter++;
return;
}

if (systemContext.GetHoldings(this).Any() && tradesLeft > 0)
{
tradesLeft = Sell(systemContext, tradesLeft);
}

cash = systemContext.GetCurrentCash(this);

if (tradesLeft > 0 && cash > 0)
{
tradesLeft = Buy(systemContext, cash, currentDate, tradesLeft);
}
}

_counter++;
}

private int Buy(ITraderSystemContext systemContext, decimal cash, DateOnly currentDate, int tradesLeft)
{
var listing = _listingPicker.GetXBestListingForDate(1, currentDate, cash);
var pricePoint = listing?.PricePoints.FirstOrDefault(l => l.Date == currentDate);

if (pricePoint is not null && cash >= pricePoint.Price)
{
systemContext.BuyStock(this, listing, (int)(cash / pricePoint.Price));

Check warning on line 59 in Bots/AleixBot/AleixBot.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference argument for parameter 'listing' in 'bool ITraderSystemContext.BuyStock(ITraderBot traderBot, IStockListing listing, int amount)'.
tradesLeft--;
}

return tradesLeft;
}

private int Sell(ITraderSystemContext systemContext, int tradesLeft)
{
var holding = systemContext.GetHoldings(this).OrderByDescending(h => h.Amount).FirstOrDefault();
if (holding != null)
{
systemContext.SellStock(this, holding.Listing, holding.Amount);
tradesLeft--;
}

return tradesLeft;
}

private static void InitializeListingPicker(System.Collections.ObjectModel.ReadOnlyCollection<IStockListing> listings, int windowSize)
{
var sw = new Stopwatch();
sw.Start();
_listingPicker = new ListingPicker(listings, windowSize);
sw.Stop();
Debug.WriteLine($"Building ListingPicker took {sw.ElapsedMilliseconds} milliseconds");
}
}
14 changes: 13 additions & 1 deletion Bots/AleixBot/AleixBot.sln
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.12.35527.113 d17.12
VisualStudioVersion = 17.12.35527.113
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AleixBot", "AleixBot.csproj", "{4EED0EB0-3BBC-41E5-A1F1-AB1C2A9DDB36}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AleixBot.Tests", "..\AleixBot.Tests\AleixBot.Tests.csproj", "{716FB5B8-EB7F-4FC1-90C7-7FD2FAE3DBA4}"
EndProject

Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -15,8 +18,17 @@ Global
{4EED0EB0-3BBC-41E5-A1F1-AB1C2A9DDB36}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4EED0EB0-3BBC-41E5-A1F1-AB1C2A9DDB36}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4EED0EB0-3BBC-41E5-A1F1-AB1C2A9DDB36}.Release|Any CPU.Build.0 = Release|Any CPU
{716FB5B8-EB7F-4FC1-90C7-7FD2FAE3DBA4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{716FB5B8-EB7F-4FC1-90C7-7FD2FAE3DBA4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{716FB5B8-EB7F-4FC1-90C7-7FD2FAE3DBA4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{716FB5B8-EB7F-4FC1-90C7-7FD2FAE3DBA4}.Release|Any CPU.Build.0 = Release|Any CPU

EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection

GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C00082EE-EABB-41B9-A550-6A1DEB725E5E}
EndGlobalSection
EndGlobal
89 changes: 89 additions & 0 deletions Bots/AleixBot/ListingPicker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using AleixBot.Models;
using NasdaqTrader.Bot.Core;
using System.Collections.ObjectModel;

namespace AleixBot;

public class ListingPicker
{
public ReadOnlyCollection<IStockListing> Listings { get; }
public int WindowSize { get; }
public Collection<IntermediateListing> IntermediateListings { get; } = [];
public List<WindowedSumPerListingPerDate> WindowedListingSumPerDate = [];

private List<Period> _periods = [];

public ListingPicker(ReadOnlyCollection<IStockListing> listings, int windowSize)
{
Listings = listings;
WindowSize = windowSize;
ConstructIntermediateDictionary();
}

public void ConstructIntermediateDictionary()
{

foreach (var listing in Listings)
{
for (int i = 0; i < listing.PricePoints.Length - 2; i++)
{
var date = listing.PricePoints[i].Date;
var priceDifference = listing.PricePoints[i + 1].Price - listing.PricePoints[i].Price;
IntermediateListings.Add(new IntermediateListing(listing.Name, date, priceDifference, listing.PricePoints[i].Price));
}
}

var allDates = IntermediateListings.Select(il => il.Date);
if (!allDates.Any()) return;

DateOnly begindate = allDates.Min();
DateOnly endDate = allDates.Max();

var groupedIntermediateListings = IntermediateListings
.GroupBy(il => il.Name)
.ToDictionary(g => g.Key, g => g.ToList());

DateOnly j = begindate;
while (j.AddDays(WindowSize) < endDate)
{
_periods.Add(new Period(j, j.AddDays(WindowSize)));
j = j.AddDays(WindowSize);
}

foreach (var (name, listings) in groupedIntermediateListings)
{
foreach (var period in _periods)
{
// Filter once per group and period
var windowSum = listings
.Where(il => il.Date >= period.Start && il.Date <= period.End)
.Sum(s => s.PriceDifference);

var priceOnStartDate = listings
.Where(il => il.Date == period.Start)
.Select(il => il.Price)
.FirstOrDefault();

WindowedListingSumPerDate.Add(new WindowedSumPerListingPerDate(period.Start, name, windowSum, priceOnStartDate));
}
}
}

public IStockListing? GetXBestListingForDate(int number, DateOnly date, decimal cash)
{
var periodForDate = _periods.Where(p => p.Start <= date && p.End >= date).FirstOrDefault();

if (periodForDate == null)
{
return null;
}

var bestListingName = WindowedListingSumPerDate
.Where(wl => wl.Date == periodForDate.Start && wl.PriceOnStartDate < cash)
.OrderByDescending(wl => wl.Sum)
.Select(wl => wl.Name)
.FirstOrDefault();

return Listings.FirstOrDefault(l => l.Name.Equals(bestListingName));
}
}
5 changes: 5 additions & 0 deletions Bots/AleixBot/Models/IntermediateListing.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using NasdaqTrader.Bot.Core;

namespace AleixBot.Models;

public record IntermediateListing(string Name, DateOnly Date, decimal PriceDifference, decimal Price);
3 changes: 3 additions & 0 deletions Bots/AleixBot/Models/Period.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace AleixBot.Models;

internal record Period(DateOnly Start, DateOnly End);
3 changes: 3 additions & 0 deletions Bots/AleixBot/Models/WindowedSumPerListingPerDate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace AleixBot.Models;

public record WindowedSumPerListingPerDate(DateOnly Date, string Name, decimal Sum, decimal PriceOnStartDate);
Loading