using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using Game.Extensions; namespace Game { /// /// Provides a method for simulating a stock market. /// public static class StockMarket { /// /// Simulates changes in the stock market over time. /// /// /// The collection of companies that will participate in the market. /// /// /// An infinite sequence of trading days. Each day represents the /// state of the stock market at the start of that day. /// public static IEnumerable Simulate(ImmutableArray companies) { var random = new Random(); var cyclicParameters = EnumerableExtensions.Zip( Trends(random, 1, 5), PriceSpikes(random, companies.Length, 1, 5), PriceSpikes(random, companies.Length, 1, 5), (trend, company1, company2) => (trend, positiveSpike: company1, negativeSpike: company2)); return cyclicParameters.SelectAndAggregate( new TradingDay { Companies = companies }, (parameters, previousDay) => previousDay with { Companies = previousDay.Companies.Map( (company, index) => AdjustSharePrice( random, company, parameters.trend, parameters.positiveSpike == index, parameters.negativeSpike == index)) }); } /// /// Creates a copy of a company with a randomly adjusted share price, /// based on the given parameters. /// /// /// The random number generator. /// /// /// The company to adjust. /// /// /// The slope of the overall market price trend. /// /// /// True if the function should simulate a positive spike in the /// company's share price. /// /// /// True if the function should simulate a negative spike in the /// company's share price. /// /// /// The adjusted company. /// private static Company AdjustSharePrice(Random random, Company company, double trend, bool positiveSpike, bool negativeSpike) { var boost = random.Next(4) * 0.25; var spikeAmount = 0.0; if (positiveSpike) spikeAmount = 10; if (negativeSpike) spikeAmount = spikeAmount - 10; var priceChange = (int)(trend * company.SharePrice) + boost + (int)(3.5 - (6 * random.NextDouble())) + spikeAmount; var newPrice = company.SharePrice + priceChange; if (newPrice < 0) newPrice = 0; return company with { SharePrice = newPrice }; } /// /// Generates an infinite sequence of market trends. /// /// /// The random number generator. /// /// /// The minimum number of days each trend should last. /// /// /// The maximum number of days each trend should last. /// public static IEnumerable Trends(Random random, int minDays, int maxDays) => random.Integers(minDays, maxDays + 1).SelectMany(daysInCycle => Enumerable.Repeat(GenerateTrend(random), daysInCycle)); /// /// Generates a random value for the market trend. /// /// /// The random number generator. /// /// /// A trend value in the range [-0.1, 0.1]. /// private static double GenerateTrend(Random random) => ((int)(random.NextDouble() * 10 + 0.5) / 100.0) * (random.Next(2) == 0 ? 1 : -1) ; /// /// Generates an infinite sequence of price spikes. /// /// /// The random number generator. /// /// /// The number of companies. /// /// /// The minimum number of days in between price spikes. /// /// /// The maximum number of days in between price spikes. /// /// /// An infinite sequence of random company indexes and null values. /// A non-null value means that the corresponding company should /// experience a price spike. /// private static IEnumerable PriceSpikes(Random random, int companyCount, int minDays, int maxDays) => random.Integers(minDays, maxDays + 1) .SelectMany( daysInCycle => Enumerable.Range(0, daysInCycle), (daysInCycle, dayNumber) => dayNumber == 0 ? random.Next(companyCount) : default(int?)); } }