diff --git a/.gitignore b/.gitignore index d2880c1..2bcb9a8 100644 --- a/.gitignore +++ b/.gitignore @@ -131,4 +131,15 @@ $RECYCLE.BIN/ # Mac desktop service store files .DS_Store -_NCrunch* \ No newline at end of file +_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 diff --git a/Bots/AleixBot/AleixBot.cs b/Bots/AleixBot/AleixBot.cs index 28c230f..81100e1 100644 --- a/Bots/AleixBot/AleixBot.cs +++ b/Bots/AleixBot/AleixBot.cs @@ -1,10 +1,16 @@ 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; + private static int _counter = 0; + private const int WINDOWSIZE = 3; public async Task DoTurn(ITraderSystemContext systemContext) { @@ -13,6 +19,68 @@ public async Task DoTurn(ITraderSystemContext systemContext) 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)); + 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 listings, int windowSize) + { + var sw = new Stopwatch(); + sw.Start(); + _listingPicker = new ListingPicker(listings, windowSize); + sw.Stop(); + Debug.WriteLine($"Building ListingPicker took {sw.ElapsedMilliseconds} milliseconds"); } } \ No newline at end of file diff --git a/Bots/AleixBot/AleixBot.sln b/Bots/AleixBot/AleixBot.sln index 9fb532a..f9a0d2a 100644 --- a/Bots/AleixBot/AleixBot.sln +++ b/Bots/AleixBot/AleixBot.sln @@ -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 @@ -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 diff --git a/Bots/AleixBot/ListingPicker.cs b/Bots/AleixBot/ListingPicker.cs new file mode 100644 index 0000000..fa8abf4 --- /dev/null +++ b/Bots/AleixBot/ListingPicker.cs @@ -0,0 +1,89 @@ +using AleixBot.Models; +using NasdaqTrader.Bot.Core; +using System.Collections.ObjectModel; + +namespace AleixBot; + +public class ListingPicker +{ + public ReadOnlyCollection Listings { get; } + public int WindowSize { get; } + public Collection IntermediateListings { get; } = []; + public List WindowedListingSumPerDate = []; + + private List _periods = []; + + public ListingPicker(ReadOnlyCollection 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)); + } +} \ No newline at end of file diff --git a/Bots/AleixBot/Models/IntermediateListing.cs b/Bots/AleixBot/Models/IntermediateListing.cs new file mode 100644 index 0000000..1321ee4 --- /dev/null +++ b/Bots/AleixBot/Models/IntermediateListing.cs @@ -0,0 +1,5 @@ +using NasdaqTrader.Bot.Core; + +namespace AleixBot.Models; + +public record IntermediateListing(string Name, DateOnly Date, decimal PriceDifference, decimal Price); \ No newline at end of file diff --git a/Bots/AleixBot/Models/Period.cs b/Bots/AleixBot/Models/Period.cs new file mode 100644 index 0000000..8cdfc81 --- /dev/null +++ b/Bots/AleixBot/Models/Period.cs @@ -0,0 +1,3 @@ +namespace AleixBot.Models; + +internal record Period(DateOnly Start, DateOnly End); \ No newline at end of file diff --git a/Bots/AleixBot/Models/WindowedSumPerListingPerDate.cs b/Bots/AleixBot/Models/WindowedSumPerListingPerDate.cs new file mode 100644 index 0000000..7538119 --- /dev/null +++ b/Bots/AleixBot/Models/WindowedSumPerListingPerDate.cs @@ -0,0 +1,3 @@ +namespace AleixBot.Models; + +public record WindowedSumPerListingPerDate(DateOnly Date, string Name, decimal Sum, decimal PriceOnStartDate); \ No newline at end of file