9

I am creating a stock trading simulator where the last days's trade price is taken as opening price and simulated through out the current day.

For that I am generating random double numbers that may be somewhere -5% of lastTradePrice and 5% above the lastTradePrice. However after around 240 iterations I see how the produced double number gets smaller and smaller closing to zero.

Random rand = new Random();
Thread.Sleep(rand.Next(0,10));
Random random = new Random();
double lastTradeMinus5p = model.LastTradePrice - model.LastTradePrice * 0.05;
double lastTradePlus5p = model.LastTradePrice + model.LastTradePrice * 0.05;
model.LastTradePrice = random.NextDouble() * (lastTradePlus5p - lastTradeMinus5p) + lastTradeMinus5p;

As you can see I am trying to get random seed by utilising Thread.sleep(). And yet its not truly randomised. Why is there this tendency to always produce smaller numbers?

enter image description here

Update:

The math itself is actually fine, despite the downwards trend as Jon has proven it. Getting random double numbers between range is also explained here.

The real problem was the seed of Random. I have followed Jon's advice to keep the same Random instance across the thread for all three prices. And this already is producing better results; the price is actually bouncing back upwards. I am still investigating and open to suggestions how to improve this. The link Jon has given provides an excellent article how to produce a random instance per thread.

Btw the whole project is open source if you are interested. (Using WCF, WPF in Browser, PRISM 4.2, .NET 4.5 Stack)

The TransformPrices call is happening here on one separate thread.

This is what happens if I keep the same instance of random: enter image description here

And this is generated via RandomProvider.GetThreadRandom(); as pointed out in the article: enter image description here

Community
  • 1
  • 1
Houman
  • 64,245
  • 87
  • 278
  • 460

5 Answers5

11

Firstly, calling Thread.Sleep like this is not a good way of getting a different seed. It would be better to use a single instance of Random per thread. See my article on randomness for some suggested approaches.

However, your code is also inherently biased downwards. Suppose we "randomly" get 0.0 and 1.0 from the random number generator, starting with a price of $100. That will give:

  • Day 0: $100
  • Day 1: $95 (-5% = $5)
  • Day 2: $99.75 (+5% = $4.75)

Now we can equally randomly get 1.0 and 0.0:

  • Day 0: $100
  • Day 1: $105 (+5% = $5)
  • Day 2: $99.75 (-5% = $5.25)

Note how we've got down in both cases, despite this being "fair". If the value increases, that means it can go down further on the next roll of the dice, so to speak... but if the value decreases, it can't bounce back as far.

EDIT: To give an idea of how a "reasonably fair" RNG is still likely to give a decreasing value, here's a little console app:

using System;

class Test
{
    static void Main()
    {
        Random random = new Random();
        int under100 = 0;
        for (int i = 0; i < 100; i++)
        {
            double price = 100;
            double sum = 0;

            for (int j = 0; j < 1000; j++)
            {
                double lowerBound = price * 0.95;
                double upperBound = price * 1.05;
                double sample = random.NextDouble();
                sum += sample;
                price = sample * (upperBound - lowerBound) + lowerBound;                
            }
            Console.WriteLine("Average: {0:f2} Price: {1:f2}", sum / 1000, price);
            if (price < 100)
            {
                under100++;
            }
        }
        Console.WriteLine("Samples with a final price < 100: {0}", under100);
    }
}

On my machine, the "average" value is always very close to 0.5 (rarely less then 0.48 or more than 0.52) but the majority of "final prices" are always below 100 - about 65-70% of them.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • Thanks for the explanation. While I agree that math could be problematic, your suggestion with keeping one random instance across the code, helped me a lot. See updated question. (Btw your link doesn't work) FYI I have the project as open source, if you are keen to see how the method is called, maybe you have a better idea how to improve the math: https://github.com/houmie/StockTrading/blob/master/Service/Service.cs#L155 I am always open for suggestions and improvements. Cheers – Houman Feb 25 '14 at 13:24
  • @Hooman: Fixed the link now. (It was just doubled.) If you could end up with multiple threads calling that method at the same time, they could still end up with the same seed. Follow the fixed link to see how to avoid that. – Jon Skeet Feb 25 '14 at 13:26
  • Thanks the article was great. `RandomProvider.GetThreadRandom();` is very useful and I am now utilising it. I have updated the results in the question. Hence even though the math shows a downwards trend, it doesn't have to go down necessarily as the two new screenshots prove. e.g. in your example, if you get twice `1.0` you end up with 110.25 for Day 2. Do you agree? Thanks – Houman Feb 25 '14 at 17:53
  • @Hooman: Yes, *if* you get 1.0 twice, you get to 110.25. But the point is that with a *fair* random number generator, I would expect it to still go down, for the reason shown in my answer. It's easy enough to check that - call `NextDouble()` and store the result in a `double`, so you can use it for your existing computation *and* you can sum all the values. I suspect the sum of the values will be about 120 after 240 iterations. I'm surprised by your new graphs, if you've kept the same logic. I'll investigate it myself :) – Jon Skeet Feb 25 '14 at 17:57
  • @Hooman: See my edit for a console app showing that my reasoning of "it will trend down using this maths, even if the RNG is fair" seems to be borne out. – Jon Skeet Feb 25 '14 at 18:07
  • Thanks for the console app sample. Its quite interesting. So the downward trend is still quite strong. I'll keep an eye on this. Feel free to download https://github.com/houmie/StockTrading and see the graphs live for yourself. :) Cheers – Houman Feb 25 '14 at 18:18
  • @Hooman: Well the graphs will only show a few lines - the nice thing about the console app is that you can easily tell it to try it over 1000 or 10,000 "stocks". It's very easy to get misled by just seeing 3 lines which look okay (or look bad). – Jon Skeet Feb 25 '14 at 18:19
  • @Hooman: On each iteration, you're multiplying `model.LastTradePrice` by some random number. It sounds like you want the product of numbers to have an expected value of 1, so the logarithm of the product should have an expected value of 0, so the sum of the logarithms should have an expected value of 0, so the expected value of the logarithm of each number should be 0. – Quartermeister Feb 25 '14 at 18:58
  • Let the *logarithm* be something like `random.NextDouble() * 0.10 - 0.05` so that it has an expected value of 0, and then calculate the multiplier as `Math.Exp(log)`, for something like `model.LastTradePrice *= Math.Exp(random.NextDouble() * 0.10 - 0.05);`. – Quartermeister Feb 25 '14 at 18:58
  • @Quartermeister Thanks a lot for this input. Feel free to put this all as a separate answer and I will investigate it and come back to you. Many Thanks for your help – Houman Feb 25 '14 at 19:44
1

Quick guess: This is a math-thing, and not really related to the random generator.

When you reduce the trade price by 5%, you get a resulting value that is lower than that which you began with (obviously!).

The problem is that when you then increase the trade price by 5% of that new value, those 5% will be a smaller value than the 5% you reduced by previously, since you started out with a smaller value this time. Get it?

I obviously haven't verified this, but I have strong hunch this is your problem. When you repeat these operations a bunch of times, the effect will get noticeable over time.

Kjartan
  • 18,591
  • 15
  • 71
  • 96
1

Your math should be:

double lastTradeMinus5p = model.LastTradePrice * 0.95;
double lastTradePlus5p = model.LastTradePrice * (1/0.95);

UPDATE: As Dialecticus pointed out, you should probably use some other distribution than this one:

random.NextDouble() * (lastTradePlus5p - lastTradeMinus5p)

Also, your range of 5% seems pretty narrow to me.

Dusan
  • 5,000
  • 6
  • 41
  • 58
  • 1
    True, but not enough. Random values between these two limits should be something else than evenly distributed, which they currently are with expression `random.NextDouble() * (lastTradePlus5p - lastTradeMinus5p)`. – Dialecticus Feb 25 '14 at 11:00
  • Agree, he should probably use Normal (Gaussian) distribution or something like that. – Dusan Feb 25 '14 at 11:09
0

I think this is mainly because the random number generator you are using is technically pants.

For better 'randomness' use RNGCryptoServiceProvider to generate the random numbers instead. It's technically a pseudo-random number generated, but the quality of 'randomness' is much higher (suitable for cryptographic purposes).

Taken from here

//The following sample uses the Cryptography class to simulate the roll of a dice.

using System;
using System.IO;
using System.Text;
using System.Security.Cryptography;

class RNGCSP
{
    private static RNGCryptoServiceProvider rngCsp = new RNGCryptoServiceProvider();
    // Main method. 
    public static void Main()
    {
        const int totalRolls = 25000;
        int[] results = new int[6];

        // Roll the dice 25000 times and display 
        // the results to the console. 
        for (int x = 0; x < totalRolls; x++)
        {
            byte roll = RollDice((byte)results.Length);
            results[roll - 1]++;
        }
        for (int i = 0; i < results.Length; ++i)
        {
            Console.WriteLine("{0}: {1} ({2:p1})", i + 1, results[i], (double)results[i] / (double)totalRolls);
        }
        rngCsp.Dispose();
        Console.ReadLine();
    }

    // This method simulates a roll of the dice. The input parameter is the 
    // number of sides of the dice. 

    public static byte RollDice(byte numberSides)
    {
        if (numberSides <= 0)
            throw new ArgumentOutOfRangeException("numberSides");

        // Create a byte array to hold the random value. 
        byte[] randomNumber = new byte[1];
        do
        {
            // Fill the array with a random value.
            rngCsp.GetBytes(randomNumber);
        }
        while (!IsFairRoll(randomNumber[0], numberSides));
        // Return the random number mod the number 
        // of sides.  The possible values are zero- 
        // based, so we add one. 
        return (byte)((randomNumber[0] % numberSides) + 1);
    }

    private static bool IsFairRoll(byte roll, byte numSides)
    {
        // There are MaxValue / numSides full sets of numbers that can come up 
        // in a single byte.  For instance, if we have a 6 sided die, there are 
        // 42 full sets of 1-6 that come up.  The 43rd set is incomplete. 
        int fullSetsOfValues = Byte.MaxValue / numSides;

        // If the roll is within this range of fair values, then we let it continue. 
        // In the 6 sided die case, a roll between 0 and 251 is allowed.  (We use 
        // < rather than <= since the = portion allows through an extra 0 value). 
        // 252 through 255 would provide an extra 0, 1, 2, 3 so they are not fair 
        // to use. 
        return roll < numSides * fullSetsOfValues;
    }
}
Paul Zahra
  • 9,522
  • 8
  • 54
  • 76
0

According to your code, I can derive it in a simpler version as below:

Random rand = new Random();
Thread.Sleep(rand.Next(0,10));
Random random = new Random();
double lastTradeMinus5p = model.LastTradePrice * 0.95; // model.LastTradePrice - model.LastTradePrice * 0.05 => model.LastTradePrice * ( 1 - 0.05 )
double lastTradePlus5p = model.LastTradePrice * 1.05; // model.LastTradePrice + model.LastTradePrice * 0.05 => model.LastTradePrice * ( 1 + 0.05 )
model.LastTradePrice = model.LastTradePrice * ( random.NextDouble() * 0.1 + 0.95 ) // lastTradePlus5p - lastTradeMinus5p => ( model.LastTradePrice * 1.05 ) - ( model.LastTradePrice * 0.95 ) => model.LastTradePrice * ( 1.05 - 0.95)

So you are taking model.LastTradePrice times a fractional number(between 0 to 1) times 0.1 which will always decrease more to zero, but increase less to 1 !

The litle fraction positive part comes because of the + 0.95 part with the zero-tending random.NextDouble() * 0.1

Wasif Hossain
  • 3,900
  • 1
  • 18
  • 20