Merge pull request #303 from pgruderman/main

Ported Stock Market to C#
This commit is contained in:
Jeff Atwood
2021-06-21 11:11:59 -07:00
committed by GitHub
13 changed files with 812 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.31321.278
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Game", "Game.csproj", "{BADD262D-D540-431F-8803-2A6F80C22033}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{BADD262D-D540-431F-8803-2A6F80C22033}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BADD262D-D540-431F-8803-2A6F80C22033}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BADD262D-D540-431F-8803-2A6F80C22033}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BADD262D-D540-431F-8803-2A6F80C22033}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C78DBA4A-87E2-4B31-A261-4AEF5E4C3B12}
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,20 @@
using System.Collections.Immutable;
namespace Game
{
/// <summary>
/// Stores the player's assets.
/// </summary>
public record Assets
{
/// <summary>
/// Gets the player's amount of cash.
/// </summary>
public double Cash { get; init; }
/// <summary>
/// Gets the number of stocks owned of each company.
/// </summary>
public ImmutableArray<int> Portfolio { get; init; }
}
}

View File

@@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
namespace Game
{
/// <summary>
/// Contains functions for exchanging assets.
/// </summary>
public static class Broker
{
/// <summary>
/// Applies the given set of transactions to the given set of assets.
/// </summary>
/// <param name="assets">
/// The assets to update.
/// </param>
/// <param name="transactions">
/// The set of stocks to purchase or sell. Positive values indicate
/// purchaes and negative values indicate sales.
/// </param>
/// <param name="companies">
/// The collection of companies.
/// </param>
/// <returns>
/// Returns the sellers new assets and a code indicating the result
/// of the transaction.
/// </returns>
public static (Assets newAssets, TransactionResult result) Apply(Assets assets, IEnumerable<int> transactions, IEnumerable<Company> companies)
{
var (netCost, transactionSize) = Enumerable.Zip(
transactions,
companies,
(amount, company) => (amount * company.SharePrice))
.Aggregate(
(netCost: 0.0, transactionSize: 0.0),
(accumulated, amount) => (accumulated.netCost + amount, accumulated.transactionSize + Math.Abs(amount)));
var brokerageFee = 0.01 * transactionSize;
var newAssets = assets with
{
Cash = assets.Cash - netCost - brokerageFee,
Portfolio = ImmutableArray.CreateRange(Enumerable.Zip(
assets.Portfolio,
transactions,
(sharesOwned, delta) => sharesOwned + delta))
};
if (newAssets.Portfolio.Any(amount => amount < 0))
return (newAssets, TransactionResult.Oversold);
else
if (newAssets.Cash < 0)
return (newAssets, TransactionResult.Overspent);
else
return (newAssets, TransactionResult.Ok);
}
}
}

View File

@@ -0,0 +1,23 @@
namespace Game
{
/// <summary>
/// Represents a company.
/// </summary>
public record Company
{
/// <summary>
/// Gets the company's name.
/// </summary>
public string Name { get; init; }
/// <summary>
/// Gets the company's three letter stock symbol.
/// </summary>
public string StockSymbol { get; init; }
/// <summary>
/// Gets the company's current share price.
/// </summary>
public double SharePrice { get; init; }
}
}

View File

@@ -0,0 +1,107 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Game
{
public static class Controller
{
/// <summary>
/// Manages the initial interaction with the user.
/// </summary>
public static void StartGame()
{
View.ShowBanner();
var showInstructions = GetYesOrNo(View.PromptShowInstructions);
View.ShowSeparator();
if (showInstructions)
View.ShowInstructions();
View.ShowSeparator();
}
/// <summary>
/// Gets a yes or no answer from the user.
/// </summary>
/// <param name="prompt">
/// Displays the prompt.
/// </param>
/// <returns>
/// True if the user answered yes and false if he or she answered no.
/// </returns>
public static bool GetYesOrNo(Action prompt)
{
prompt();
var response = default(char);
do
{
response = Console.ReadKey(intercept: true).KeyChar;
}
while (response != '0' && response != '1');
View.ShowChar(response);
return response == '1';
}
/// <summary>
/// Gets a transaction amount for each company in the given collection
/// of companies and returns the updated assets.
/// </summary>
/// <param name="assets">
/// The assets to update.
/// </param>
/// <param name="companies">
/// The collection of companies.
/// </param>
/// <returns>
/// The updated assets.
/// </returns>
public static Assets UpdateAssets(Assets assets, IEnumerable<Company> companies)
{
while (true)
{
View.PromptEnterTransactions();
var result = Broker.Apply (
assets,
companies.Select(GetTransactionAmount).ToList(),
companies);
switch (result)
{
case (Assets newAssets, TransactionResult.Ok):
return newAssets;
case (_, TransactionResult.Oversold):
View.ShowOversold();
break;
case (Assets newAssets, TransactionResult.Overspent):
View.ShowOverspent(-newAssets.Cash);
break;
}
}
}
/// <summary>
/// Gets a transaction amount for the given company.
/// </summary>
/// <param name="company">
/// The company to buy or sell.
/// </param>
/// <returns>
/// The number of shares to buy or sell.
/// </returns>
public static int GetTransactionAmount(Company company)
{
while (true)
{
View.PromptBuySellCompany(company);
if (Int32.TryParse(Console.ReadLine(), out var amount))
return amount;
else
View.PromptValidInteger();
}
}
}
}

View File

@@ -0,0 +1,116 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Game.Extensions
{
/// <summary>
/// Provides additional methods for the <see cref="IEnumerable{T}"/>
/// interface.
/// </summary>
public static class EnumerableExtensions
{
/// <summary>
/// Simultaneously projects each element of a sequence and applies
/// the result of the previous projection.
/// </summary>
/// <typeparam name="TSource">
/// The type of elements in the source sequence.
/// </typeparam>
/// <typeparam name="TResult">
/// The type of elements in the result sequence.
/// </typeparam>
/// <param name="source">
/// The source sequence.
/// </param>
/// <param name="seed">
/// The seed value for the aggregation component. This value is
/// passed to the first call to <paramref name="selector"/>.
/// </param>
/// <param name="selector">
/// The projection function. This function is supplied with a value
/// from the source sequence and the result of the projection on the
/// previous value in the source sequence.
/// </param>
/// <returns>
/// The resulting sequence.
/// </returns>
public static IEnumerable<TResult> SelectAndAggregate<TSource, TResult>(
this IEnumerable<TSource> source,
TResult seed,
Func<TSource, TResult, TResult> selector)
{
foreach (var element in source)
{
seed = selector(element, seed);
yield return seed;
}
}
/// <summary>
/// Combines the results of three distinct sequences into a single
/// sequence.
/// </summary>
/// <typeparam name="T1">
/// The element type of the first sequence.
/// </typeparam>
/// <typeparam name="T2">
/// The element type of the second sequence.
/// </typeparam>
/// <typeparam name="T3">
/// The element type of the third sequence.
/// </typeparam>
/// <typeparam name="TResult">
/// The element type of the resulting sequence.
/// </typeparam>
/// <param name="first">
/// The first source sequence.
/// </param>
/// <param name="second">
/// The second source sequence.
/// </param>
/// <param name="third">
/// The third source sequence.
/// </param>
/// <param name="resultSelector">
/// Function that combines results from each source sequence into a
/// final result.
/// </param>
/// <returns>
/// A sequence of combined values.
/// </returns>
/// <remarks>
/// <para>
/// This function works identically to Enumerable.Zip except that it
/// combines three sequences instead of two.
/// </para>
/// <para>
/// We have defined this as an extension method for consistency with
/// the similar LINQ methods in the <see cref="Enumerable"/> class.
/// However, since there is nothing special about the first sequence,
/// it is often more clear to call this as a regular function. For
/// example:
/// </para>
/// <code>
/// EnumerableExtensions.Zip(
/// sequence1,
/// sequence2,
/// sequence3,
/// (a, b, c) => GetResult (a, b, c));
/// </code>
/// </remarks>
public static IEnumerable<TResult> Zip<T1, T2, T3, TResult>(
this IEnumerable<T1> first,
IEnumerable<T2> second,
IEnumerable<T3> third,
Func<T1, T2, T3, TResult> resultSelector)
{
using var enumerator1 = first.GetEnumerator();
using var enumerator2 = second.GetEnumerator();
using var enumerator3 = third.GetEnumerator();
while (enumerator1.MoveNext() && enumerator2.MoveNext() && enumerator3.MoveNext())
yield return resultSelector(enumerator1.Current, enumerator2.Current, enumerator3.Current);
}
}
}

View File

@@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
namespace Game.Extensions
{
/// <summary>
/// Provides additional methods for the <see cref="Random"/> class.
/// </summary>
public static class RandomExtensions
{
/// <summary>
/// Generates an infinite sequence of random numbers.
/// </summary>
/// <param name="random">
/// The random number generator.
/// </param>
/// <param name="min">
/// The inclusive lower bound of the range to generate.
/// </param>
/// <param name="max">
/// The exclusive upper bound of the range to generate.
/// </param>
/// <returns>
/// An infinite sequence of random integers in the range [min, max).
/// </returns>
/// <remarks>
/// <para>
/// We use an exclusive upper bound, even though it's a little
/// confusing, for the sake of consistency with Random.Next.
/// </para>
/// <para>
/// Since the sequence is infinite, a typical usage would be to cap
/// the results with a function like Enumerable.Take. For example,
/// to sum the results of rolling three six sided dice, we could do:
/// </para>
/// <code>
/// random.Integers(1, 7).Take(3).Sum()
/// </code>
/// </remarks>
public static IEnumerable<int> Integers(this Random random, int min, int max)
{
while (true)
yield return random.Next(min, max);
}
}
}

View File

@@ -0,0 +1,52 @@
using System;
using System.Collections.Immutable;
using System.Linq;
namespace Game
{
class Program
{
/// <summary>
/// Defines the set of companies that will be simulated in the game.
/// </summary>
private static ImmutableArray<Company> Companies = ImmutableArray.CreateRange(new[]
{
new Company { Name = "INT. BALLISTIC MISSILES", StockSymbol = "IBM", SharePrice = 100 },
new Company { Name = "RED CROSS OF AMERICA", StockSymbol = "RCA", SharePrice = 85 },
new Company { Name = "LICHTENSTEIN, BUMRAP & JOKE", StockSymbol = "LBJ", SharePrice = 150 },
new Company { Name = "AMERICAN BANKRUPT CO.", StockSymbol = "ABC", SharePrice = 140 },
new Company { Name = "CENSURED BOOKS STORE", StockSymbol = "CBS", SharePrice = 110 }
});
static void Main()
{
var assets = new Assets
{
Cash = 10000.0,
Portfolio = ImmutableArray.CreateRange(Enumerable.Repeat(0, Companies.Length))
};
var previousDay = default(TradingDay);
Controller.StartGame();
foreach (var day in StockMarket.Simulate(Companies))
{
if (previousDay is null)
View.ShowCompanies(day.Companies);
else
View.ShowTradeResults(day, previousDay, assets);
View.ShowAssets(assets, day.Companies);
if (previousDay is not null && !Controller.GetYesOrNo(View.PromptContinue))
break;
assets = Controller.UpdateAssets(assets, day.Companies);
previousDay = day;
}
View.ShowFarewell();
}
}
}

View File

@@ -0,0 +1,149 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Game.Extensions;
namespace Game
{
/// <summary>
/// Provides a method for simulating a stock market.
/// </summary>
public static class StockMarket
{
/// <summary>
/// Simulates changes in the stock market over time.
/// </summary>
/// <param name="companies">
/// The collection of companies that will participate in the market.
/// </param>
/// <returns>
/// An infinite sequence of trading days. Each day represents the
/// state of the stock market at the start of that day.
/// </returns>
public static IEnumerable<TradingDay> Simulate(ImmutableArray<Company> 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 = ImmutableArray.CreateRange(
previousDay.Companies.Select ((company, index) => AdjustSharePrice(
random,
company,
parameters.trend,
parameters.positiveSpike == index,
parameters.negativeSpike == index)))
});
}
/// <summary>
/// Creates a copy of a company with a randomly adjusted share price,
/// based on the given parameters.
/// </summary>
/// <param name="random">
/// The random number generator.
/// </param>
/// <param name="company">
/// The company to adjust.
/// </param>
/// <param name="trend">
/// The slope of the overall market price trend.
/// </param>
/// <param name="positiveSpike">
/// True if the function should simulate a positive spike in the
/// company's share price.
/// </param>
/// <param name="negativeSpike">
/// True if the function should simulate a negative spike in the
/// company's share price.
/// </param>
/// <returns>
/// The adjusted company.
/// </returns>
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 };
}
/// <summary>
/// Generates an infinite sequence of market trends.
/// </summary>
/// <param name="random">
/// The random number generator.
/// </param>
/// <param name="minDays">
/// The minimum number of days each trend should last.
/// </param>
/// <param name="maxDays">
/// The maximum number of days each trend should last.
/// </param>
public static IEnumerable<double> Trends(Random random, int minDays, int maxDays) =>
random.Integers(minDays, maxDays + 1).SelectMany(days => Enumerable.Repeat(GenerateTrend(random), days));
/// <summary>
/// Generates a random value for the market trend.
/// </summary>
/// <param name="random">
/// The random number generator.
/// </param>
/// <returns>
/// A trend value in the range [-0.1, 0.1].
/// </returns>
private static double GenerateTrend(Random random) =>
((int)(random.NextDouble() * 10 + 0.5) / 100.0) * (random.Next(2) == 0 ? 1 : -1) ;
/// <summary>
/// Generates an infinite sequence of price spikes.
/// </summary>
/// <param name="random">
/// The random number generator.
/// </param>
/// <param name="companyCount">
/// The number of companies.
/// </param>
/// <param name="minDays">
/// The minimum number of days in between price spikes.
/// </param>
/// <param name="maxDays">
/// The maximum number of days in between price spikes.
/// </param>
/// <returns>
/// An infinite sequence of random company indexes and null values.
/// A non-null value means that the corresponding company should
/// experience a price spike.
/// </returns>
private static IEnumerable<int?> PriceSpikes(Random random, int companyCount, int minDays, int maxDays) =>
random.Integers(minDays, maxDays + 1)
.SelectMany(
days => Enumerable.Range(0, days),
(days, dayNumber) => dayNumber == 0 ? random.Next(companyCount) : default(int?));
}
}

View File

@@ -0,0 +1,24 @@
using System.Collections.Immutable;
using System.Linq;
namespace Game
{
/// <summary>
/// Represents a single trading day.
/// </summary>
public record TradingDay
{
/// <summary>
/// Gets the average share price of all companies in the market this
/// day.
/// </summary>
public double AverageSharePrice =>
Companies.Average (company => company.SharePrice);
/// <summary>
/// Gets the collection of public listed companies in the stock market
/// this day.
/// </summary>
public ImmutableArray<Company> Companies { get; init; }
}
}

View File

@@ -0,0 +1,25 @@
namespace Game
{
/// <summary>
/// Enumerates the different possible outcomes of applying a transaction.
/// </summary>
public enum TransactionResult
{
/// <summary>
/// The transaction was successful.
/// </summary>
Ok,
/// <summary>
/// The transaction failed because the seller tried to sell more shares
/// than he or she owns.
/// </summary>
Oversold,
/// <summary>
/// The transaction failed because the net cost was greater than the
/// seller's available cash.
/// </summary>
Overspent
}
}

View File

@@ -0,0 +1,157 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Game.Extensions;
namespace Game
{
/// <summary>
/// Contains functions for displaying information to the user.
/// </summary>
public static class View
{
public static void ShowBanner()
{
Console.WriteLine(" STOCK MARKET");
Console.WriteLine(" CREATIVE COMPUTING MORRISTOWN, NEW JERSEY");
Console.WriteLine();
Console.WriteLine();
Console.WriteLine();
}
public static void ShowInstructions()
{
Console.WriteLine("THIS PROGRAM PLAYS THE STOCK MARKET. YOU WILL BE GIVEN");
Console.WriteLine("$10,000 AND MAY BUY OR SELL STOCKS. THE STOCK PRICES WILL");
Console.WriteLine("BE GENERATED RANDOMLY AND THEREFORE THIS MODEL DOES NOT");
Console.WriteLine("REPRESENT EXACTLY WHAT HAPPENS ON THE EXCHANGE. A TABLE");
Console.WriteLine("OF AVAILABLE STOCKS, THEIR PRICES, AND THE NUMBER OF SHARES");
Console.WriteLine("IN YOUR PORTFOLIO WILL BE PRINTED. FOLLOWING THIS, THE");
Console.WriteLine("INITIALS OF EACH STOCK WILL BE PRINTED WITH A QUESTION");
Console.WriteLine("MARK. HERE YOU INDICATE A TRANSACTION. TO BUY A STOCK");
Console.WriteLine("TYPE +NNN, TO SELL A STOCK TYPE -NNN, WHERE NNN IS THE");
Console.WriteLine("NUMBER OF SHARES. A BROKERAGE FEE OF 1% WILL BE CHARGED");
Console.WriteLine("ON ALL TRANSACTIONS. NOTE THAT IF A STOCK'S VALUE DROPS");
Console.WriteLine("TO ZERO IT MAY REBOUND TO A POSITIVE VALUE AGAIN. YOU");
Console.WriteLine("HAVE $10,000 TO INVEST. USE INTEGERS FOR ALL YOUR INPUTS.");
Console.WriteLine("(NOTE: TO GET A 'FEEL' FOR THE MARKET RUN FOR AT LEAST");
Console.WriteLine("10 DAYS)");
Console.WriteLine("-----GOOD LUCK!-----");
}
public static void ShowCompanies(IEnumerable<Company> companies)
{
var maxNameLength = companies.Max(company => company.Name.Length);
Console.WriteLine($"{"STOCK".PadRight(maxNameLength)} INITIALS PRICE/SHARE");
foreach (var company in companies)
Console.WriteLine($"{company.Name.PadRight(maxNameLength)} {company.StockSymbol} {company.SharePrice:0.00}");
Console.WriteLine();
Console.WriteLine($"NEW YORK STOCK EXCHANGE AVERAGE: {companies.Average(company => company.SharePrice):0.00}");
Console.WriteLine();
}
public static void ShowTradeResults(TradingDay day, TradingDay previousDay, Assets assets)
{
var results = EnumerableExtensions.Zip(
day.Companies,
previousDay.Companies,
assets.Portfolio,
(company, previous, shares) =>
(
stockSymbol: company.StockSymbol,
price: company.SharePrice,
shares,
value: shares * company.SharePrice,
change: company.SharePrice - previous.SharePrice
)).ToList();
Console.WriteLine();
Console.WriteLine();
Console.WriteLine("********** END OF DAY'S TRADING **********");
Console.WriteLine();
Console.WriteLine();
Console.WriteLine("STOCK\tPRICE/SHARE\tHOLDINGS\tVALUE\tNET PRICE CHANGE");
foreach (var result in results)
Console.WriteLine($"{result.stockSymbol}\t{result.price}\t\t{result.shares}\t\t{result.value:0.00}\t\t{result.change:0.00}");
Console.WriteLine();
Console.WriteLine();
Console.WriteLine();
var averagePrice = day.AverageSharePrice;
var averagePriceChange = averagePrice - previousDay.AverageSharePrice;
Console.WriteLine($"NEW YORK STOCK EXCHANGE AVERAGE: {averagePrice:0.00} NET CHANGE {averagePriceChange:0.00}");
Console.WriteLine();
}
public static void ShowAssets(Assets assets, IEnumerable<Company> companies)
{
var totalStockValue = Enumerable.Zip(
assets.Portfolio,
companies,
(shares, company) => shares * company.SharePrice).Sum();
Console.WriteLine($"TOTAL STOCK ASSETS ARE ${totalStockValue:0.00}");
Console.WriteLine($"TOTAL CASH ASSETS ARE ${assets.Cash:0.00}");
Console.WriteLine($"TOTAL ASSETS ARE ${totalStockValue + assets.Cash:0.00}");
Console.WriteLine();
}
public static void ShowOversold()
{
Console.WriteLine();
Console.WriteLine("YOU HAVE OVERSOLD A STOCK; TRY AGAIN.");
}
public static void ShowOverspent(double amount)
{
Console.WriteLine();
Console.WriteLine($"YOU HAVE USED ${amount:0.00} MORE THAN YOU HAVE.");
}
public static void ShowFarewell()
{
Console.WriteLine("HOPE YOU HAD FUN!!");
}
public static void ShowSeparator()
{
Console.WriteLine();
Console.WriteLine();
}
public static void ShowChar(char c)
{
Console.WriteLine(c);
}
public static void PromptShowInstructions()
{
Console.Write("DO YOU WANT THE INSTRUCTIONS (YES-TYPE 1, NO-TYPE 0)? ");
}
public static void PromptContinue()
{
Console.Write("DO YOU WISH TO CONTINUE (YES-TYPE 1, NO-TYPE 0)? ");
}
public static void PromptEnterTransactions()
{
Console.WriteLine("WHAT IS YOUR TRANSACTION IN");
}
public static void PromptBuySellCompany(Company company)
{
Console.Write($"{company.StockSymbol}? ");
}
public static void PromptValidInteger()
{
Console.WriteLine("PLEASE ENTER A VALID INTEGER");
}
}
}