Lean  $LEAN_TAG$
RandomValueGenerator.cs
1 /*
2  * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3  * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software
10  * distributed under the License is distributed on an "AS IS" BASIS,
11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  * See the License for the specific language governing permissions and
13  * limitations under the License.
14 */
15 
17 using QuantConnect.Util;
18 using System;
19 using System.Linq;
20 
22 {
23  /// <summary>
24  /// Provides an implementation of <see cref="IRandomValueGenerator"/> that uses
25  /// <see cref="Random"/> to generate random values
26  /// </summary>
28  {
29  private readonly Random _random;
30  private readonly MarketHoursDatabase _marketHoursDatabase;
31  private readonly SymbolPropertiesDatabase _symbolPropertiesDatabase;
32  private const decimal _maximumPriceAllowed = 1000000m;
33 
34 
35  public RandomValueGenerator()
36  : this(new Random())
37  { }
38 
39  public RandomValueGenerator(int seed)
40  : this(new Random(seed))
41  { }
42 
43  public RandomValueGenerator(Random random)
45  { }
46 
47  public RandomValueGenerator(
48  int seed,
49  MarketHoursDatabase marketHoursDatabase,
50  SymbolPropertiesDatabase symbolPropertiesDatabase
51  )
52  : this(new Random(seed), marketHoursDatabase, symbolPropertiesDatabase)
53  { }
54 
55  public RandomValueGenerator(Random random, MarketHoursDatabase marketHoursDatabase, SymbolPropertiesDatabase symbolPropertiesDatabase)
56  {
57  _random = random;
58  _marketHoursDatabase = marketHoursDatabase;
59  _symbolPropertiesDatabase = symbolPropertiesDatabase;
60  }
61 
62  public bool NextBool(double percentOddsForTrue)
63  {
64  return _random.NextDouble() <= percentOddsForTrue / 100;
65  }
66 
67  public virtual DateTime NextDate(DateTime minDateTime, DateTime maxDateTime, DayOfWeek? dayOfWeek)
68  {
69  if (maxDateTime < minDateTime)
70  {
71  throw new ArgumentException(
72  "The maximum date time must be less than or equal to the minimum date time specified"
73  );
74  }
75 
76  // compute a random date time value
77  var rangeInDays = (int)maxDateTime.Subtract(minDateTime).TotalDays;
78  var daysOffsetFromMin = _random.Next(0, rangeInDays);
79  var dateTime = minDateTime.AddDays(daysOffsetFromMin);
80 
81  var currentDayOfWeek = dateTime.DayOfWeek;
82  if (!dayOfWeek.HasValue || currentDayOfWeek == dayOfWeek.Value)
83  {
84  // either DOW wasn't specified or we got REALLY lucky, although, I suppose it'll happen 1/7 (~14%) of the time
85  return dateTime;
86  }
87 
88  var nextDayOfWeek = Enumerable.Range(0, 7)
89  .Select(i => dateTime.AddDays(i))
90  .First(dt => dt.DayOfWeek == dayOfWeek.Value);
91 
92  var previousDayOfWeek = Enumerable.Range(0, 7)
93  .Select(i => dateTime.AddDays(-i))
94  .First(dt => dt.DayOfWeek == dayOfWeek.Value);
95 
96  // both are valid dates, so chose one randomly
97  if (IsWithinRange(nextDayOfWeek, minDateTime, maxDateTime) &&
98  IsWithinRange(previousDayOfWeek, minDateTime, maxDateTime)
99  )
100  {
101  return _random.Next(0, 1) == 0
102  ? previousDayOfWeek
103  : nextDayOfWeek;
104  }
105 
106  if (IsWithinRange(nextDayOfWeek, minDateTime, maxDateTime))
107  {
108  return nextDayOfWeek;
109  }
110 
111  if (IsWithinRange(previousDayOfWeek, minDateTime, maxDateTime))
112  {
113  return previousDayOfWeek;
114  }
115 
116  throw new ArgumentException("The provided min and max dates do not have the requested day of week between them");
117  }
118 
119  public double NextDouble() => _random.NextDouble();
120 
121  public int NextInt(int minValue, int maxValue) => _random.Next(minValue, maxValue);
122 
123  public int NextInt(int maxValue) => _random.Next(maxValue);
124 
125  /// <summary>
126  /// Generates a random <see cref="decimal"/> suitable as a price. This should observe minimum price
127  /// variations if available in <see cref="SymbolPropertiesDatabase"/>, and if not, truncating to 2
128  /// decimal places.
129  /// </summary>
130  /// <exception cref="ArgumentException">Throw when the <paramref name="referencePrice"/> or <paramref name="maximumPercentDeviation"/>
131  /// is less than or equal to zero.</exception>
132  /// <param name="securityType">The security type the price is being generated for</param>
133  /// <param name="market">The market of the security the price is being generated for</param>
134  /// <param name="referencePrice">The reference price used as the mean of random price generation</param>
135  /// <param name="maximumPercentDeviation">The maximum percent deviation. This value is in percent space,
136  /// so a value of 1m is equal to 1%.</param>
137  /// <returns>A new decimal suitable for usage as price within the specified deviation from the reference price</returns>
138  public virtual decimal NextPrice(SecurityType securityType, string market, decimal referencePrice, decimal maximumPercentDeviation)
139  {
140  if (referencePrice <= 0)
141  {
142  if (securityType == SecurityType.Option && referencePrice == 0)
143  {
144  return 0;
145  }
146  throw new ArgumentException("The provided reference price must be a positive number.");
147  }
148 
149  if (maximumPercentDeviation <= 0)
150  {
151  throw new ArgumentException("The provided maximum percent deviation must be a positive number");
152  }
153 
154  // convert from percent space to decimal space
155  maximumPercentDeviation /= 100m;
156 
157  var symbolProperties = _symbolPropertiesDatabase.GetSymbolProperties(market, null, securityType, "USD");
158  var minimumPriceVariation = symbolProperties.MinimumPriceVariation;
159 
160  decimal price;
161  var attempts = 0;
162  var increaseProbabilityFactor = 0.5;
163  do
164  {
165  // what follows is a simple model of browning motion that
166  // limits the walk to the specified percent deviation
167 
168  var deviation = referencePrice * maximumPercentDeviation * (decimal)(NextDouble() - increaseProbabilityFactor);
169  deviation = Math.Sign(deviation) * Math.Max(Math.Abs(deviation), minimumPriceVariation);
170  price = referencePrice + deviation;
171  price = RoundPrice(price, minimumPriceVariation);
172 
173  if (price < 20 * minimumPriceVariation)
174  {
175  // The price should not be to close to the minimum price variation.
176  // Invalidate the price to try again and increase the probability of it to going up
177  price = -1m;
178  increaseProbabilityFactor = Math.Max(increaseProbabilityFactor - 0.05, 0);
179  }
180 
181  if (price > (_maximumPriceAllowed / 10m))
182  {
183  // The price should not be too higher
184  // Decrease the probability of it to going up
185  increaseProbabilityFactor = increaseProbabilityFactor + 0.05;
186  }
187 
188  if (price > _maximumPriceAllowed)
189  {
190  // The price should not be too higher
191  // Invalidate the price to try again
192  price = -1;
193  }
194  } while (!IsPriceValid(securityType, price) && ++attempts < 10);
195 
196  if (!IsPriceValid(securityType, price))
197  {
198  // if still invalid, use the last price
199  price = referencePrice;
200  }
201 
202  return price;
203  }
204 
205  private static decimal RoundPrice(decimal price, decimal minimumPriceVariation)
206  {
207  if (minimumPriceVariation == 0) return minimumPriceVariation;
208  return Math.Round(price / minimumPriceVariation) * minimumPriceVariation;
209  }
210 
211  private bool IsWithinRange(DateTime value, DateTime min, DateTime max)
212  {
213  return value >= min && value <= max;
214  }
215 
216  private static bool IsPriceValid(SecurityType securityType, decimal price)
217  {
218  switch (securityType)
219  {
220  case SecurityType.Option:
221  {
222  return price >= 0;
223  }
224  default:
225  {
226  return price > 0 && price < _maximumPriceAllowed;
227  }
228  }
229  }
230  }
231 }