Sonification: Stock Market Music

Hi all,

I wrote a Sonic Pi script that sonifies stock market prices. Here is a video that gives you an idea of what it sounds like. More info is available in the video description. If there is interest, I can post the code.

https://youtu.be/mcwId_RA0rc

6 Likes

Definitely interested in the code!!

Very cool.

1 Like

Brilliant project, thanks for sharing!

I’d also love to see the code at some point.

I would like to see the code. How are you consuming the stock market prices?

Thanks, @ts-swx and @Davetheword. I swatted some bugs, pulled out the test print statements, added comments, etc., and here is the code. A few notes:

I was pleasantly surprised at the musicality of the output. It generally has a calm, flowing, meditative quality. Not really what you’d expect from a sonification of stock market data.

However, it is not surprising that it has a human quality. It’s not random music; stock market price movement is the cumulative opinion of a large group of people. A random number series does not exhibit the same characteristics. There is much academic writing about this, but it’s not important here.

<I’m not sure how to upload the CSV file that contains the stock market data. If someone could tell me, I will do so>

Download the CSV file that I have attached. Change the path that you see in the user-definable variables section to where you put the file.

I put most of the user-definable variables at the top of the code. Changing any parameter yields a new musical rendition of the underlying dataset. Changing the BPM, scale, or duration is pretty straightforward. Changing the start date, or the number of bars, will change the period of the dataset used by the script. The longer the period you use, the wider the dispersion of prices will probably be. The width of dispersion of prices dictates how many notes are held. If you set bars to a large number, you will probably get many held notes and a more static performance. Shorter settings generally yield more active performances.

What really makes this script different from merely assigning prices to notes, which sounds boring pretty quickly, are the notes that are held (tied) across bars, and the dynamic control over the notes. For the latter I thank @robin.newman, whose code I adapted to control amplitude levels via the FX loop. In conjunction with the :amp_slide argument, volume levels are translated into beautiful crescendos and decrescendos.

The tied note idea came to me when I noticed that there were repeated notes after the scaling of prices to notes routine ran (see the Create Note Arrays section in the code). I decided to apply the voice leading technique of tying notes when they repeat. This is usually used on inner voices, but, as used here, it smoothes out the note progression nicely.

Overall, although this version of the script is still sonification of stock prices, I have become more interested in the musical aspects of the process. I originally created it to answer for myself (it’s certainly been done before) what stock price movement sounds like. Any enhancement should increase the clarity of the aural translation. A decision like tying the notes across bars, I believe, reduces this clarity. But, I like what it sounds like. So, I think I’ll continue developing it from a musical perspective. For example, the code that handles the preprocessing necessary to determine the length of notes can be used with a dataset other than stock prices, one of intentionally musical phrases.

Let me know if you have questions, or if you do something interesting with the code.

# ==================  SPYmusic4_forPublish.rb  ========================
require 'csv'
require 'matrix'
# =========User-defined variables ===========================
# location of data file
data = CSV.read('g:/My Documents/sonic-pi/spy.csv')
use_bpm 60
dur= 1
skale = :phrygian # Scale to use
#numNotes = 28 # 4 octaves of major scale
numNotes = 21  # 3 octaves of major scale
#numNotes = 14 # 2 octaves of major scale
#numNotes = 7  # one octave of major scale
octaves = 3       # number of octaves to use
bars = 93 # bars of price data to use for the "song".
# NOTE: inputting a non-existent date throws an error.
# Just pick a different date.
startDate = "2/15/2006" # bars = 93

# Here's some interesting time zones to listen in ---------
#startDate = "7/22/2005" # bars = 93
#startDate = "7/22/2005" # bars = 140
#startDate = "2/3/2005"
#startDate = "5/2/2005" # bars = 140

# -- Info on the included SPY.csv file----------------------
# SPY.csv: 1/2/2000 - 4/16/2012
# Each bar = a chord
# SPY.csv has 3091 bars
# ===========End of user-defined variables ==================

date = Array.new
open = Array.new
high  = Array.new
low = Array.new
close = Array.new
volume = Array.new
rowArray = Array.new
prcRng = 0
volRng = 0
noteRng = 0
hiPrc = 0
lowPrc = 0
hiVolume = 0
lowVolume = 0

# Read CSV file, set up price arrays-------------------------

rowArray = data
rowArray.shift
rowArray.reverse! # for backwards yahoo data
s = rowArray.length
print "size:", s

# Find index of startDate
first = Matrix[*rowArray].index(startDate)[0]

# if bars > EOF then truncate bars
if first + bars > s
  last = s
  bars = s - first
else
  last = first + bars
end
print  first, last, bars
row = first

i = 0

until row >= last do
  date[i] = rowArray[row][0].to_s
  #open[i] = rowArray[row][1].to_f
  high[i] = rowArray[row][2].to_f
  low[i] = rowArray[row][3].to_f
  close[i] = rowArray[row][4].to_f
  volume[i] = (rowArray[row][5].to_f) #/volFactor
  i+=1
  row+=1
end

# ------------- Price and volume calculations ----------------
hiPrc = high.max.round(2)
lowPrc = low.min.round(2)
hiVolume = volume.max
lowVolume = volume.min
prcRng = (hiPrc-lowPrc).round(2)
prcVolRng = hiVolume - lowVolume
nteRng = numNotes
nteVol = 1.0

print "hiPrc: " + hiPrc.to_s
print "lowPrc: " + lowPrc.to_s
print "price range:"+ prcRng.to_s
print "Price Volume range:"+ prcVolRng.to_s
print "note range:"+ nteRng.to_s
print "bars: " + bars.to_s

# Create note arrays -----------------------------
define :noteArrays do |hlc, notes, counts, volumes|
  count = 1
  i = 0
  num = 0
  
  hlc.each_with_index do |num, i|
    prc_i = (num - lowPrc)/prcRng
    notes[i] = (prc_i*numNotes).round
  end
  
  notes.each_with_index do |num, index|
    if num == notes[index+1]
      count += 1
    else
      counts << count
      while i <= count -2
        counts << 0
        i += 1
      end
      count = 1
      i = 0
    end
  end
  spark (notes)
  
end
# -------------------------------------------------
define :sing do |nte, nteLen, oct|
  play (scale oct, skale, num_octaves: octaves)[nte],
    vibrato_delay: (nteLen) * 0.5, vibrato_depth: rrand(0.08,0.15),
    vibrato_onset: rrand(0.08,1), vibrato_rate: rrand(4.0,6.0),
    sustain: nteLen*dur, release: rrand(0.08,0.8), attack: rrand(0.2,0.8)
end

print "------------------------------------------------"

#------ low counts ------------------------------
lowNotes = []
lowCounts = []
lowVolumes = []
noteArrays low, lowNotes, lowCounts, lowVolumes

#------ close counts ----------------------------
closeNotes = []
closeCounts = []
closeVolumes = []
noteArrays close, closeNotes,closeCounts, closeVolumes

# ----- high counts -----------------------------
highNotes = []
highCounts = []
highVolumes = []
noteArrays high, highNotes, highCounts, highVolumes
# -----------------------------------------------

tick_reset
print "Here we go!"
#==================MAIN LOOP ===============================
with_fx :reverb, mix: 1, room: 0.75 do
  with_fx :level, amp: 0 do |v| #use fx :level to adjust volume
    set :vref, v #store reference to fx :level
    
    bars.times do
      tick
      prcVol = volume[look]
      nteAmp = ((prcVol - lowVolume)/prcVolRng) * 0.2 # *0.2 prevents distorting
      control get(:vref), amp: nteAmp, amp_slide: 1
      hHor, lHor, cHor = 0, 0, 0
      
      # --------------  LOW  ----------------------------------
      lNote = lowNotes[look]
      lNoteLen = lowCounts[look]
      
      pan = -1
      use_synth  :blade
      lOct =note(:d3)
      if lNoteLen > 0
        sing lNote, lNoteLen, lOct
      end
      lHor = lNote + lOct
      # ---------- CLOSE ---------------------------------------
      cNote = closeNotes[look]
      cNoteLen = closeCounts[look]
      
      pan = 0
      use_synth :blade
      cOct = note(:d3)
      if cNoteLen > 0
        sing cNote, cNoteLen, cOct
      end
      cHor = cNote + cOct
      #---------------  HIGH  ---------------------------------
      hNote = highNotes[look]
      hNoteLen = highCounts[look]
      
      pan = 1
      use_synth  :blade
      hOct = note(:d3)
      if hNoteLen > 0
        sing hNote, hNoteLen, hOct
      end
      hHor = hNote + hOct
      # ---------- Log Output ----------------------------------
      
      print date[look]
      print note_info(scale(lOct, skale, num_octaves: octaves)[lNote]).midi_string.center(7),
        note_info(scale(cOct, skale, num_octaves: octaves)[cNote]).midi_string.center(7),
        note_info(scale(hOct, skale, num_octaves: octaves)[hNote]).midi_string.center(7),
        "#{' ' * (lHor-lOct) }L #{' ' * (cHor-lHor) }C #{' ' * (hHor-cHor) }H"
      print low[look].to_s.rjust(7),
        close[look].to_s.rjust(7),
        high[look].to_s.rjust(7),
        '-' * (((prcVol - lowVolume)/prcVolRng)*numNotes).ceil
      wait dur
    end
  end
end

5 Likes

You made my month. I’ve really been wanting to get do stuff like this and reading the code is incredibly helpful.

@Ken as far as uploading the csv goes, my only thought would be a dropbox or google drive link. I just grabbed the data off of yahoo and am working on some debugging. I’ve gotten it to work a couple of times but I’m now getting an error on

    notes[i] = (prc_i*numNotes).round

it’s mentioning the .round. I haven’t a clue but I will be doing my best to work through it :slight_smile:. It may have something to do with the fact I grabbed a different time period, and am using weekly data rather than daily?

The error I’m getting says Runtime Error - Float Domain Error, Thread Death! NaN

@ ts-swx I was going to try it with weekly data. That would give you a bigger range of prices for the script to chew on. I got very interested in the repeating note aspect, which calls for more repeating prices. So I stuck with daily prices, since the narrower dispersion of those would yield more repeating notes. But, if you use weekly prices, by keeping numNotessmall, you’d probably still get some repeating notes. Anyway, I still haven’t tried it, so I’m interested in your results.

Regarding the error, it looks like notes[i] might be choking on something in your dataset. Could there be a missing entry? It’s hard to debug problems when there is external data involved. Of course, there could be a script bug, but I would go over your data first.

I am going to chop out of a piece of the CSV file that I’m using and upload it here. Then folks can create their own CSV file from it. Try using that data and let me know how it goes.

1 Like

Since we can’t upload files, here is a section of the CSV file that I used in the script. Copy the lines into a file and call it <YourFileName>.CSV. Then change the line in the script to that location and file name.

6/15/2006,123.95,126.36,123.86,126.12,134057000,111.88
6/14/2006,122.84,123.63,122.34,123.5,163566400,109.56
6/13/2006,123.74,124.84,122.55,122.55,185688800,108.72
6/12/2006,125.88,125.93,123.82,123.99,95815900,109.99
6/9/2006,126.36,126.96,125.29,125.35,94972200,111.2
6/8/2006,125.58,126.5,123.87,125.75,204957200,111.56
6/7/2006,126.91,127.65,125.79,125.86,108599400,111.65
6/6/2006,127.21,127.38,125.76,126.81,130498600,112.5
6/5/2006,128.85,128.86,126.77,127.12,86105100,112.77
6/2/2006,129.25,129.43,128.32,129,91702600,114.44
6/1/2006,127.38,128.94,127.27,128.73,73721700,114.2
5/31/2006,126.62,127.51,126.2,127.51,86926200,113.12
5/30/2006,127.97,128,126.05,126.1,72419900,111.87
5/26/2006,128.01,128.38,127.51,128.38,62989700,113.89
5/25/2006,126.92,127.73,126.43,127.73,78977900,113.31
5/24/2006,125.68,126.89,124.76,126.17,168405000,111.93
5/23/2006,127.18,127.63,125.17,125.17,92006500,111.04
5/22/2006,126.28,127.17,125.5,126.13,110852800,111.89
5/19/2006,126.87,127.49,125.8,127.1,124309400,112.75
5/18/2006,127.35,127.75,126.11,126.21,87906300,111.96
5/17/2006,128.67,129.1,126.77,126.85,144789500,112.53
5/16/2006,129.76,130,129.01,129.31,62137600,114.71
5/15/2006,128.79,129.74,128.61,129.5,84029300,114.88
5/12/2006,130.36,130.72,129.19,129.24,91726500,114.65
5/11/2006,132.51,132.55,130.52,130.95,80626900,116.17
5/10/2006,132.41,132.75,131.89,132.55,64378200,117.59
5/9/2006,132.42,132.77,132.31,132.62,29864000,117.65
5/8/2006,132.51,132.77,132.36,132.36,30016700,117.42
5/5/2006,132.05,132.8,131.85,132.52,62588200,117.56
5/4/2006,131.08,131.62,130.97,131.36,42921400,116.53
5/3/2006,131.15,131.32,130.45,130.89,60821300,116.11
5/2/2006,131.01,131.46,130.74,131.38,49063500,116.55
5/1/2006,131.47,131.8,130.32,130.4,64990300,115.68
4/28/2006,130.79,131.75,130.71,131.47,55854400,116.63
4/27/2006,129.9,131.63,129.59,131.03,124478600,116.24
4/26/2006,130.5,131.14,130.3,130.4,67262400,115.68
4/25/2006,131.04,131.12,129.92,130.37,84359800,115.65
4/24/2006,130.89,131.07,130.38,130.91,52546400,116.13
4/21/2006,131.69,131.79,130.62,131.15,72342600,116.35
4/20/2006,131,131.86,130.6,131.13,86005500,116.33
4/19/2006,130.75,131.07,130.24,130.95,87269000,116.17
4/18/2006,128.93,130.94,128.93,130.7,92531800,115.95
4/17/2006,128.83,129.31,128.02,128.66,64167700,114.14
4/13/2006,128.59,129.25,128.31,128.71,51051800,114.18
4/12/2006,128.77,129.13,128.61,128.88,43033700,114.33
4/11/2006,129.85,130.06,128.25,128.64,72799400,114.12
4/10/2006,129.74,130.08,129.26,129.74,41496500,115.09
4/7/2006,131.06,131.4,129.35,129.54,80180900,114.92
4/6/2006,130.85,131.21,130.19,130.87,57906200,116.1
4/5/2006,130.61,131.28,130.38,131.01,50607200,116.22
4/4/2006,129.73,130.73,129.36,130.56,54809300,115.82
4/3/2006,130.07,130.87,129.49,129.73,61624700,115.09
3/31/2006,130.02,130.24,129.37,129.83,62925600,115.17
3/30/2006,130.11,130.98,129.55,129.8,70571700,115.15
3/29/2006,129.41,130.5,129.29,130.03,61505700,115.35
3/28/2006,129.93,130.53,129.05,129.22,82079900,114.63
3/27/2006,130.03,130.28,129.74,130.02,32523000,115.34
3/24/2006,129.99,130.57,129.74,130.21,43209200,115.51
3/23/2006,130.26,130.39,129.66,130.11,46704200,115.42
3/22/2006,129.51,130.51,129.45,130.38,51605700,115.66
3/21/2006,130.37,130.99,129.45,129.59,87102700,114.96
3/20/2006,130.64,130.9,130.21,130.41,45538500,115.69
3/17/2006,130.68,130.9,130.38,130.62,47286800,115.88
3/16/2006,131.01,131.47,130.84,131.03,65526400,115.78
3/15/2006,130.15,130.86,129.85,130.76,53398900,115.54
3/14/2006,128.71,130.23,128.61,130.18,69877300,115.03
3/13/2006,128.84,129.16,128.53,128.83,45479100,113.83
3/10/2006,127.71,128.84,127.44,128.59,60490800,113.62
3/9/2006,128.28,128.68,127.38,127.38,56313600,112.55
3/8/2006,127.7,128.44,127.18,128.24,66692400,113.31
3/7/2006,127.86,128.06,127.4,127.97,61780800,113.07
3/6/2006,129.14,129.18,127.85,128.17,57478400,113.25
3/3/2006,128.67,130.07,128.65,128.76,73402500,113.77
3/2/2006,128.9,129.42,128.61,129.36,60642300,114.3
3/1/2006,128.6,129.49,128.5,129.37,48641600,114.31
2/28/2006,129.2,129.91,128.13,128.23,74394800,113.3
2/27/2006,129.4,130.04,129.28,129.46,35858600,114.39
2/24/2006,129.11,129.48,128.76,129.41,36777400,114.35
2/23/2006,129.27,129.64,128.28,129.08,43423200,114.06
2/22/2006,128.77,129.65,128.65,129.27,42326700,114.22
2/21/2006,129.11,129.4,128.29,128.49,46456300,113.53
2/17/2006,129.05,129.16,128.58,128.81,40342600,113.82
2/16/2006,128.34,129.21,128.18,129.16,61017900,114.13
2/15/2006,127.68,128.32,127.24,128.2,85471300,113.28

I have written some Processing code that displays graphics for the SP stock market sonification script. Various aspects of the display are controlled by the pitch, timing, and amplitude values sent via OSC from SP. Here is a YouTube video that shows the results.

https://youtu.be/mJTUZtKgVAw

Any comments are welcome.

1 Like

these are both awesome. I got swamped with some work stuff and a little distracted with Bespoke Synth since the UI was really helpful for learning some aspects of music theory, but I’m gonna play around with this script again some time this week.