LiveCode Stock Market

Posted by Scott McDonald

Here is the first simulation game at the LiveCode Game Developer. In this game your goal is to make money by buying and selling stocks (commonly called shares in some countries) in 5 companies at a stock exchange. The game is called Stock Market and it simulates the changing stock prices of Apple, Google, IBM, Intel and Microsoft. Your goal is to buy stocks when values are rising, and sell them before they fall to make a virtual profit. You start with $10,000 in virtual play money and hope to increase your total value after each day of trading (buying and selling).

This game is based on the Stock Market game in BASIC Computer Games from 1978, but has been updated with a better interface than the original and the significant addition of a chart showing the changes in each stock over time.

In this game you will see:

  • How to draw a line chart without a complex library
  • LiveCode associative arrays used to simplify code
  • Error checking of user input
  • The Any and Random keywords used to simulate stock price movements
  • Rounding of numbers, while keeping trailing zeros after the decimal point

Interestingly, adding a chart to the game was much easier than figuring out the logic of the original program. The BASIC source from 1978 was a written in a version that did not have switch statements or the option of an else part to the if statement. Reading the old code after enjoying the benefits of a structured language like LiveCode (with proper functions and decent control structures) was a chore.

Lengthy sequences of if then gotos were used to work around the absence of an else and switch statement. (To be fair to the BASIC language, I suspect much of the original code could have been simplified if more effort was put into the design.)

Enough grumbling about old code, time to look at the new.

The StartGame handler initializes some variables and then fills the sStockChartData array with 14 days of stock prices. Unlike the original program, in LiveCode Stock Market you can see the past fortnight of prices to help with your buying and selling decisions. A repeat loop like this:

repeat for each item loopStockCode in kStockCodes

is used to iterate through the 5 different stocks, using the actual name of the stock as the key (index) of the sStockChartData array. CalculateNewStockValues is called to put the current stock price into the 1 dimensional sStockValueArray, while sStockChartData is a 2 dimensional array that stores those values over the last 14 days. To make sure the vertical axis of the chart is big enough for the highest price, the local variable maxDollar keeps track of the maximum price.

I think the chart looks neater (it also turned out easier to code) when the maximum for the y axis is an even multiple of $100, so the ConvertDollarToMaxY function is used to round the price maximum upwards to the nearest multiple of 100.

function ConvertDollarToMaxY pDollar
  local maxY
  put pDollar div 100 into maxY
  if pDollar mod 100 <> 0 then add 1 to maxY
  return 100 * maxY into maxY
end ConvertDollarToMaxY

Then DrawChartBackground, DrawChartLines and UpdateDisplay are called to prepare the screen ready for the first day of trades. The reason for sPrevMaxY will be clear when DrawChartBackground is explained later below.

command StartGame
  local maxDollar,maxY
  
  lock screen
  InitializeVariables
  put 0 into maxDollar
  repeat with loopWeek = 1 to kChartXDivide + 1
    CalculateNewStockValues
    repeat for each item loopStockCode in kStockCodes
      put sStockValueArray[loopStockCode] into sStockChartData[loopStockCode,loopWeek]
      put max(sStockValueArray[loopStockCode], maxDollar) into maxDollar
    end repeat
  end repeat
  put ConvertDollarToMaxY(maxDollar) into maxY
  put 0 into sPrevMaxY
  DrawChartBackground 100,100,500,300,maxY
  DrawChartLines 100,100,500,300,maxY
  UpdateDisplay
  hide field "labeHint"
  unlock screen
end StartGame

InitializeVariables is not very interesting, but it does set the initial values for sTrendSlope and sTrendLength. LiveCode Stock Market simulates changes in stock prices assuming that there is a general trend, i.e. stocks are tending to move up or down. This is represented by whether sTrendSlope has a positive or negative value. And this trend has a set length. It would be no fun if stocks always kept going up, or down, so sTrendLength is the number of days the trend will last for. After which, the direction and slope of the movement in the stock prices may (or may not) change.

command InitializeVariables
  constant kInitialValue = "100,85,150,140,110"
  local valueIndex
  put Trunc(Random(20) - 10)/100 into sTrendSlope
  put Random(5) into sTrendLength
  put 1 into valueIndex
  repeat for each item loopStockCode in kStockCodes
    set the hilite of button ("radiBuy" & loopStockCode) to true
    put item valueIndex of kInitialValue into sStockValueArray[loopStockCode]
    put 0 into sStockHoldingArray[loopStockCode]
    add 1 to valueIndex
  end repeat
  put 0 into sDay
  put 0 into sTrendUpCounter
  put 0 into sTrendDownCounter
  put 10000 into sCash
  put 0 into sStockAssets
  put 0 into sAverage
end InitializeVariables

The CalculateNewStockValues handler uses more mathematics to determine the actual price of each stock at the end of each trading day. I have a dilemma here. If I explain this code in detail it will ruin the fun of playing the game because it will give you an advantage, increasing your chance of predicting the future prices. For this reason, details are skipped here and left as an exercise for you to figure out, once you have had a exhausted the fun of trying to get rich.

To understand this code it helps to have a basic understanding of the mathematics of line slope. (The Wikipedia article has way more details than you need for here, so you can ignore the complex information there.)

Unlike the real stock market, prices move in a random way in this simulation, so the Random and Any keywords are used extensively in calculating the new prices. An important piece of code is:

put 10 + Random(500) / 100 into stockFloor
if sStockValueArray[loopStockCode] < stockFloor then
  put stockFloor into sStockValueArray[loopStockCode]
  add (stockFloor - sStockChangeArray[loopStockCode]) to sStockChangeArray[loopStockCode]
end if

The original Stock Market game allowed a price to reach zero and included in the instructions the phrase, "If the price of a stock drops to zero it may rebound." While I am told that a stock can reach zero when a company is bankrupt, it seemed incorrect (even in a simple simulation like this) that it could then rebound. So more code was added and the lines above ensure that a stock can never fall to less than around tend dollars.

The CalculateNewStockValues also calculates the total value of your stocks by multiplying sStockHoldingArray by sStockValueArray for each company. The LiveCode associative arrays make this code simple.

command CalculateNewStockValues
  local overallFactor,trendingFactor,prevAverage,stockFloor
  if sTrendUpCounter = 0 then
    put any item of kStockCodes into sTrendUpStock
    put random(5) into sTrendUpCounter
  end if
  if sTrendDownCounter = 0 then
    put any item of kStockCodes into sTrendDownStock
    put random(5) into sTrendDownCounter
  end if
  subtract 1 from sTrendUpCounter
  subtract 1 from sTrendDownCounter
  put sAverage into prevAverage
  put 0 into sAverage
  repeat for each item loopStockCode in kStockCodes
    put random(4) * 0.25 into overallFactor
    if overallFactor = 1 then put 0 into overallFactor
    put 0 into trendingFactor
    if loopStockCode = sTrendUpStock then put 10 into trendingFactor
    if loopStockCode = sTrendDownStock then put -10 into trendingFactor
    put Trunc(sTrendSlope * sStockValueArray[loopStockCode]) + overallFactor + Trunc(3 - Random(6)) + trendingFactor into sStockChangeArray[loopStockCode]
    add sStockChangeArray[loopStockCode] to sStockValueArray[loopStockCode]
    -- don't want the stocks to fall too low
    put 10 + Random(500) / 100 into stockFloor
    if sStockValueArray[loopStockCode] < stockFloor then
      put stockFloor into sStockValueArray[loopStockCode]
      add (stockFloor - sStockChangeArray[loopStockCode]) to sStockChangeArray[loopStockCode]
    end if
    add sStockValueArray[loopStockCode] to sAverage
  end repeat
  put sAverage / 5 into sAverage
  put sAverage - prevAverage into sNetChange
  subtract 1 from sTrendLength
  if sTrendLength = 0 then
    put Trunc(Random(20) - 10)/100 into sTrendSlope
    put Random(5) into sTrendLength
  end if
  -- update total asset value
  put 0 into sStockAssets
  repeat for each item loopStockCode in kStockCodes
    add sStockHoldingArray[loopStockCode] * sStockValueArray[loopStockCode] to sStockAssets
  end repeat
end CalculateNewStockValues

DrawChartBackground draws the white rectangle as a background for the chart, and then draws the horizontal axis tick marks for each day, and the vertical axis tick marks for the dollar values. Lastly, the labels for the dollars are added. Labelling the days didn't seem necessary.

Here is where sPrevMaxY is used. There is no reason to redraw the chart background every day, but it does need to be redrawn if the vertical axis needs changing because there is a new maximum value for the highest stock price. By comparing the value of pMaxY passed to DrawChartBackground with sPrevMaxY, the chart background is only cleared and redrawn when it needs to be. While this optimisation is unnecessary for a turn based game like this, it is such a simple technique (only requiring one extra variable and an if statement) I find it difficult not to include.

Most of the work in this handler is figuring out the spacing and drawing of the tick marks and labels. The two values for pX2 and pY2 are also adjusted to ensure that the edges of white rectangle matches align perfectly with the last tick mark. This is necessary because the tick marks need to spaced out by an integer amount (i.e. a whole number), but depending on the values passed for pX1, pY1, pX2, pY2 the width and height of the rectangle may not be a perfect fit.

command DrawChartBackground pX1, pY1, pX2, pY2, pMaxY
  local xDelta,yDelta,x1,y1,x2,y2,dollarDelta
  
  if pMaxY <> sPrevMaxY then
    put pMaxY into sPrevMaxY
    repeat with loopIndex = the number of images down to 1
      if the chartBackground of image loopIndex then delete image loopIndex
    end repeat
    repeat with loopIndex = the number of graphics down to 1
      if the chartBackground of graphic loopIndex then delete graphic loopIndex
    end repeat
    repeat with loopIndex = the number of fields down to 1
      if the chartBackground of field loopIndex then delete field loopIndex
    end repeat
    
    put (pX2 - pX1) div kChartXDivide into xDelta
    put pX1 + kChartXDivide * xDelta + 1 into pX2
    put (pY2 - pY1) div kChartYDivide into yDelta
    put pY1 + kChartYDivide * yDelta + 1 into pY2
    
    put pMaxY div kChartYDivide into dollarDelta
    
    create graphic
    set the style of it to "rectangle"
    set the rect of it to pX1, pY1, pX2, pY2
    set the brushColor of it to "white"
    set the penColor of it to "black"
    set the filled of it to true
    set the chartBackground of it to true
    
    put pX1 into x1
    put x1 into x2
    put pY2 into y1
    put y1 + 4 into y2
    reset templateGraphic
    set the style of the templateGraphic to "line"
    set the penColor of the templateGraphic to "black"
    set the penWidth of the templateGraphic to 1
    repeat kChartXDivide + 1 times
      create graphic
      set the points of it to x1,y1,x2,y2
      set the chartBackground of it to true
      add xDelta to x1
      put x1 into x2
    end repeat
    
    reset templateField
    set the textFont of the templateField to "Arial"
    set the textSize of the templateField to 8
    set the textAlign of the templateField to "left"
    set the showBorder of the templateField to false
    set the style of the templateField to "transparent"
    set the margins of the templateField to "0,2,0,0"
    
    put pX2 into x1
    put x1 + 4 into x2
    put pY1 into y1
    put y1 into y2
    repeat kChartYDivide + 1 times
      create graphic
      set the points of it to x1,y1,x2,y2
      set the chartBackground of it to true
      add yDelta to y1
      put y1 into y2
      
      create field
      set the rectangle of it to x2+4,y1-21,x2+50,y1+20
      set the text of it to "$" & pMaxY
      set the chartBackground of it to true
      subtract dollarDelta from pMaxY
    end repeat
    
    reset templateGraphic
    reset templateField
  end if
end DrawChartBackground

Next in DrawChartLines the lines for the 5 stock prices are drawn. Here there is no attempt to optimise the graphics. All the existing lines are deleted and fresh ones drawn. It would be possible to just delete the 5 lines at the left end of the chart, move the remaining lines one day to the left and then add the 5 new lines at the right. But unlike the simple use of sPrevMaxY in the previous handler, this would take much more code and effort than it is worth. That is the key about optimising. Don't do it it it isn't necessary and complicates your code. Since this is a turn based game, a simple method that is quick to code, while less efficient at runtime is more than adequate.

command DrawChartLines pX1, pY1, pX2, pY2, pMaxY
  local xDelta,yDelta,x1,y1,x2,y2,dollarScale,colourIndex
  
  repeat with loopIndex = the number of graphics down to 1
    if the chartLine of graphic loopIndex then delete graphic loopIndex
  end repeat
  
  put (pX2 - pX1) div kChartXDivide into xDelta
  put pX1 + kChartXDivide * xDelta + 1 into pX2
  put (pY2 - pY1) div kChartYDivide into yDelta
  put pY1 + kChartYDivide * yDelta + 1 into pY2
  
  put (pY2 - pY1) / pMaxY into dollarScale
  
  reset templateGraphic
  set the style of the templateGraphic to "line"
  set the penWidth of the templateGraphic to 1
  put 1 into colourIndex
  repeat for each item loopStockCode in kStockCodes
    set the penColor of the templateGraphic to item colourIndex of kStockColours
    put pX1 into x1
    put x1 + xDelta into x2
    repeat with loopWeek = 2 to kChartXDivide + 1
      create graphic
      put pY2 - round(dollarScale * sStockChartData[loopStockCode,loopWeek-1]) into y1
      put pY2 - round(dollarScale * sStockChartData[loopStockCode,loopWeek]) into y2
      set the points of it to x1,y1,x2,y2
      set the chartLine of it to true
      add xDelta to x1
      add xDelta to x2
    end repeat
    add 1 to colourIndex
  end repeat
  reset templateGraphic
end DrawChartLines

One key point in DrawChartBackground and DrawChartLines is that the template objects must be reset. After using templateGraphic and templateField it is essential that you reset them. Failure to do so will almost certainly result in undesirable behaviour in your program, and even in the LiveCode IDE itself.

The FormatDollars function is called by UpdateDisplay. FormatDollars accepts a number then rounds it to 2 decimal places. Ensuring that there are two numerals after the decimal point, even if they are zeroes. Then commas are inserted to separate the thousands and lastly this is all prefixed with a dollar sign. There may be shorter ways of doing this in LiveCode, but I borrowed this code from another application of mine and I know it works. If you have code that works, re-use it when appropriate. It will save time.

function FormatDollars pNumber
  constant kPrecision = 2
  local chIndex,count,buffer
  if pNumber is a number then
    put round(pNumber,kPrecision) into pNumber
    if kPrecision > 0 then
      put offset(".",pNumber) into chIndex
      if chIndex = 0 then
        put char 1 to kPrecision + 1 of ".00000000" after pNumber
      else
        put chIndex + kPrecision - length(pNumber) into count
        if count > 0 then
          put char 1 to count of "00000000" after pNumber
        end if
      end if
    end if
    put 0 into count
    repeat with chIndex = length(pNumber) - 3 down to 1
      put char chIndex of pNumber before buffer
      add 1 to count
      if (count mod 3 = 0) and (chIndex > 1) then put comma before buffer
    end repeat
  end if
  return "$" & buffer & char -3 to -1 of pNumber
end FormatDollars

UpdateDisplay fills all the text fields with the details of your stock. Each field on the card has been named so that a single loop can iterate though the sStockValueArray and sStockHoldingArray arrays and fill in the appropriate field without much code. This is possible because each field has a name that ends in the stock code that matches the keys of the arrays.

command UpdateDisplay
  put FormatDollars(sStockAssets) into field "labeAssets"
  put FormatDollars(sCash) into field "labeCash"
  put FormatDollars(sStockAssets + sCash) into field "labeTotal"
  repeat for each item loopStockCode in kStockCodes
    put FormatDollars(sStockValueArray[loopStockCode]) into field ("labePrice" & loopStockCode)
    put sStockHoldingArray[loopStockCode] into field ("labeHolding" & loopStockCode)
    put FormatDollars(sStockHoldingArray[loopStockCode] * sStockValueArray[loopStockCode]) into field ("labeValue" & loopStockCode)
    put sStockChangeArray[loopStockCode] into field ("labeChange" & loopStockCode)
  end repeat
  add 1 to sDay
  put "Day " & sDay into field "labeDay"
  put "$" & Round(sStockAssets + sCash) into field "labeBigTotal"
  put FormatDollars(sAverage) into field "labeAverage"
  put Round(sNetChange,2) into field "labeChange"
end UpdateDisplay

All handlers so far are called directly or indirectly from StartGame. Now the game is sitting waiting for an action from you, the player. So a mouseUp handler waits patiently for the button with the Make All Trades label to be clicked. That button receives a mouseUp message when it is clicked, and since there isn't a mouseUp handler in the script of the button, the message continues along the message path where is comes to the mouseUp handler on the card. The short name of the target is the name of the clicked button, so if it is butnMakeTrades the if conditional statement executes ValidTrades to check if the trades can be done. If they can, MakeTrades, CalculateNewStockValues, UpdateChart and UpdateDisplay are called in sequence.

on mouseUp
  if the short name of the target = "butnMakeTrades" then
    if ValidTrades() then
      lock screen
      MakeTrades
      CalculateNewStockValues
      UpdateDisplay
      UpdateChart
      unlock screen
    end if
  end if
end mouseUp

ValidTrades has several loops that check the contents of the 5 editAmount fields. Each has a name that ends in the stock code, for example editAmountAAPL, editAmountGOOGL and so on, for easy processing. Checks are made to ensure that the player:

  1. Only buys or sells a positive number of stocks
  2. Has enough money to buy the stocks, after adjusting the available cash after any selling
  3. Doesn't try to sell more stocks than he or she is holding

If any one of these condition is not met, the labeHint field is shown with an appropriate message, and the function returns false.

Looking at the code you may be wondering what the numbers 0.99 and 1.01 are for. Those factors are needed to take into account that the rules of the simulation require a 1% fee to be charged on all transactions.

If ValidTrades returns true, then MakeTrades uses two similar looking loops, to change the values in sCash and sStockHoldingArray. Then CalculateNewStockValues and UpdateDisplay are called.

function ValidTrades
  local isValid,cash
  put true into isValid
  -- make sure all numbers
  repeat for each item loopStockCode in kStockCodes
    if (field ("editAmount" & loopStockCode) is not an integer) or (field ("editAmount" & loopStockCode) < 0) then
      put "Each amount must be zero or a whole positive number." into field "labeHint"
      show field "labeHint"
      focus on field ("editAmount" & loopStockCode)
      put false into isValid
      exit repeat
    end if
    --if the hilite of button ("radiSell" & loopStockCode) then
  end repeat
  put sCash into cash
  if isValid then
    -- get amount of cash after selling
    repeat for each item loopStockCode in kStockCodes
      if the hilite of button ("radiSell" & loopStockCode) then
        add field ("editAmount" & loopStockCode) * sStockValueArray[loopStockCode] * 0.99 to cash
      end if
    end repeat
    -- then check if have enough to do buying
    repeat for each item loopStockCode in kStockCodes
      if the hilite of button ("radiBuy" & loopStockCode) then
        subtract field ("editAmount" & loopStockCode) * sStockValueArray[loopStockCode] * 1.01 from cash
        if cash < 0 then
          put "You don't have enough cash to buy this much stock." into field "labeHint"
          show field "labeHint"
          focus on field ("editAmount" & loopStockCode)
          put false into isValid
        end if
      end if
    end repeat
  end if
  if isValid then
    repeat for each item loopStockCode in kStockCodes
      if the hilite of button ("radiSell" & loopStockCode) then
        if field ("editAmount" & loopStockCode) > sStockHoldingArray[loopStockCode] then
          put "You don't have that much stock to sell." into field "labeHint"
          show field "labeHint"
          focus on field ("editAmount" & loopStockCode)
          put false into isValid
        end if
      end if
    end repeat
  end if
  return isValid
end ValidTrades

Lastly, UpdateChart shifts the numbers in sStockChartData so that each key in the second dimension gets the contents of the next key. Since the keys of the second dimension are from 1 to 14 this has the effect of moving the data for the chart 1 day (the same as 1 tick mark on the horizontal axis) to the left. This is done for all 5 stocks by having nested repeat loops where the outer loop is iterating through the stock codes for the 1st dimension of the sStockChartData array.

The new stock values from sStockValueArray are put into the last index for each stock code. Then DrawChartBackground are DrawChartLines called. This completes the trading for the day showing the new chart and the total worth, ready for the player to try out their trading skills for another day.

command UpdateChart
  local maxY,stockValue,maxDollar
  repeat with loopWeek = 1 to kChartXDivide
    repeat for each item loopStockCode in kStockCodes
      put sStockChartData[loopStockCode,loopWeek + 1] into stockValue
      put stockValue into sStockChartData[loopStockCode,loopWeek]
      put max(stockValue, maxDollar) into maxDollar
    end repeat
  end repeat
  repeat for each item loopStockCode in kStockCodes
    put sStockValueArray[loopStockCode] into stockValue
    put stockValue into sStockChartData[loopStockCode,kChartXDivide + 1]
    put max(stockValue, maxDollar) into maxDollar
  end repeat
  put ConvertDollarToMaxY(maxDollar) into maxY
  DrawChartBackground 100,100,500,300,maxY
  DrawChartLines 100,100,500,300,maxY
end UpdateChart

So that completes an educational simulation in well under my goal of less than 500 lines (it's closer to 400 lines) of pure LiveCode code. It is educational as it encourages mental arithmetic while having the fun of buying and selling. If only it was as easy to make a dollar on a real stock exchange.

Download the complete LiveCode Stock Market stack made in LiveCode 6.1.2 from here: LiveCode Stock Market.livecode

Happy LiveCoding.

Credit: This version used the game Stock Market in BASIC Computer Games, 1978 edition as a starting point for the idea.

Tagged: educational intermediate simulation turn based

Friday, June 27, 2014

0 Responses

Be the first to make a comment.


COMMENTS ARE CLOSED

 

Legal Stuff

All blog posts are copyright and cannot be re-used without permission. But all the code and scripts are dedicated to the public domain. Use such code and scripts in any way you want, but I am not responsible if they don't work for you.

Contact

Further comments or feedback? You can contact me by email at: