-
Notifications
You must be signed in to change notification settings - Fork 30
Expand file tree
/
Copy pathCommandLineOptions.cs
More file actions
208 lines (174 loc) · 7.82 KB
/
Copy pathCommandLineOptions.cs
File metadata and controls
208 lines (174 loc) · 7.82 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
// Copyright (c) 2023 Philippe Matray. All rights reserved.
// This file is part of TaLibStandard.
// TaLibStandard is licensed under the GNU General Public License v3.0.
// See the LICENSE file in the project root for the full license text.
// For more information, visit https://github.com/phmatray/TaLibStandard.
namespace TechnicalAnalysis.Samples.Backtesting;
/// <summary>
/// The parsed command line of the sample.
/// </summary>
public sealed record CommandLineOptions
{
/// <summary>
/// The usage text printed by <c>--help</c> and after a parse error.
/// </summary>
public const string Usage = """
TaLibStandard - backtesting sample
Runs four indicator strategies and a buy-and-hold baseline over the same price series,
then prints a side-by-side performance comparison. With no arguments it uses a deterministic
synthetic series, so it works completely offline.
Usage:
dotnet run --project samples/TechnicalAnalysis.Samples.Backtesting [options]
Options:
--csv <path> Load bars from a CSV file instead of generating them.
Header required: Date,Open,High,Low,Close,Volume (any order).
--bars <int> Number of synthetic bars to generate. Default: 1500.
--seed <int> Seed of the synthetic series generator. Default: 20240101.
--capital <double> Starting account equity. Default: 100000.
--commission-bps <double> Commission per fill, in basis points of notional. Default: 5.
--slippage-bps <double> Slippage per fill, in basis points of price. Default: 2.
--bars-per-year <int> Annualisation constant for CAGR, volatility, Sharpe, Sortino,
Calmar. Default: 252 (daily bars, equity calendar).
--allow-short Permit short positions. Off by default, in which case a short
signal simply flattens the position.
--trade-log Print an excerpt of each strategy's round trips.
-h, --help Print this text and exit.
Examples:
dotnet run --project samples/TechnicalAnalysis.Samples.Backtesting
dotnet run --project samples/TechnicalAnalysis.Samples.Backtesting -- --seed 7 --bars 3000 --allow-short
dotnet run --project samples/TechnicalAnalysis.Samples.Backtesting -- --csv ./spy.csv --commission-bps 10
""";
/// <summary>
/// Gets a value indicating whether the user asked for the usage text.
/// </summary>
public bool ShowHelp { get; init; }
/// <summary>
/// Gets the path of the CSV file to load, or <see langword="null"/> to generate a synthetic series.
/// </summary>
public string? CsvPath { get; init; }
/// <summary>
/// Gets the number of synthetic bars to generate when no CSV file is supplied. Defaults to <c>1500</c>.
/// </summary>
public int BarCount { get; init; } = 1_500;
/// <summary>
/// Gets the seed of the synthetic series generator.
/// </summary>
public int Seed { get; init; } = SyntheticSeriesGenerator.DefaultSeed;
/// <summary>
/// Gets a value indicating whether an excerpt of the trade log should be printed for each strategy.
/// </summary>
public bool ShowTradeLog { get; init; }
/// <summary>
/// Gets the backtest options assembled from the command line.
/// </summary>
public BacktestOptions Backtest { get; init; } = new();
/// <summary>
/// Parses a command line.
/// </summary>
/// <param name="args">The raw arguments, as handed to <c>Main</c>.</param>
/// <returns>The parsed options.</returns>
/// <exception cref="ArgumentNullException"><paramref name="args"/> is <see langword="null"/>.</exception>
/// <exception cref="FormatException">An option is unknown, missing its value, or its value is not parsable or out of range.</exception>
public static CommandLineOptions Parse(IReadOnlyList<string> args)
{
ArgumentNullException.ThrowIfNull(args);
CommandLineOptions options = new();
BacktestOptions backtest = options.Backtest;
for (int i = 0; i < args.Count; i++)
{
string argument = args[i];
switch (argument)
{
case "-h":
case "--help":
return options with { ShowHelp = true };
case "--csv":
options = options with { CsvPath = NextValue(args, ref i) };
break;
case "--bars":
options = options with { BarCount = ParseInt(NextValue(args, ref i), argument, 1) };
break;
case "--seed":
options = options with { Seed = ParseInt(NextValue(args, ref i), argument, int.MinValue) };
break;
case "--capital":
backtest = backtest with { InitialCapital = ParseDouble(NextValue(args, ref i), argument) };
break;
case "--commission-bps":
backtest = backtest with { CommissionBps = ParseDouble(NextValue(args, ref i), argument) };
break;
case "--slippage-bps":
backtest = backtest with { SlippageBps = ParseDouble(NextValue(args, ref i), argument) };
break;
case "--bars-per-year":
backtest = backtest with { BarsPerYear = ParseInt(NextValue(args, ref i), argument, 1) };
break;
case "--allow-short":
backtest = backtest with { AllowShort = true };
break;
case "--trade-log":
options = options with { ShowTradeLog = true };
break;
default:
throw new FormatException(string.Format(
CultureInfo.InvariantCulture,
"Unknown option '{0}'.",
argument));
}
}
try
{
backtest.Validate();
}
catch (ArgumentException ex)
{
throw new FormatException(ex.Message, ex);
}
return options with { Backtest = backtest };
}
private static string NextValue(IReadOnlyList<string> args, ref int index)
{
if (index + 1 >= args.Count)
{
throw new FormatException(string.Format(
CultureInfo.InvariantCulture,
"Option '{0}' requires a value.",
args[index]));
}
index++;
return args[index];
}
private static int ParseInt(string text, string option, int minimum)
{
if (!int.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out int value))
{
throw new FormatException(string.Format(
CultureInfo.InvariantCulture,
"Option '{0}' expects an integer but got '{1}'.",
option,
text));
}
if (value < minimum)
{
throw new FormatException(string.Format(
CultureInfo.InvariantCulture,
"Option '{0}' expects a value of at least {1} but got {2}.",
option,
minimum,
value));
}
return value;
}
private static double ParseDouble(string text, string option)
{
if (!double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out double value))
{
throw new FormatException(string.Format(
CultureInfo.InvariantCulture,
"Option '{0}' expects a number but got '{1}'.",
option,
text));
}
return value;
}
}