How it works
The Market Audiolizer takes market price and volume data and maps it onto a musical scale. In other words, it turns prices into melodies. In this way, market analysis may be approached from the standpoint of music theory, which may be more intuitive for the listener than charts are to the trader. From the standpoint of accessibility, audiolization allows vital market information to be accessible to those with visual impairment.
Temp directories¶
Before we get started, we'll need to tell the audiolizer where it can cache price data. Set the AUDIOLIZER_TEMP
environment variable.
import os
os.environ['AUDIOLIZER_TEMP'] = '/tmp/price_data'
Loading BTC price¶
from audiolizer.audiolizer import get_history
ticker='BTC-USD'
btc = get_history(ticker, 'June 1, 2021', 'June 18, 2021', 300)
btc.head()
time | low | high | open | close | volume |
---|---|---|---|---|---|
2021-06-01 00:00:00 | 37140.9 | 37639.9 | 37276.2 | 37624.1 | 125.973 |
2021-06-01 00:05:00 | 37532.8 | 37717.5 | 37627.2 | 37572.9 | 177.163 |
2021-06-01 00:10:00 | 37546.2 | 37670.3 | 37572.8 | 37561.7 | 185.647 |
2021-06-01 00:15:00 | 37510.7 | 37643.8 | 37566.4 | 37513.7 | 157.127 |
2021-06-01 00:20:00 | 37505 | 37694.8 | 37510.7 | 37647.7 | 121.479 |
help(get_history)
Help on function get_history in module audiolizer.audiolizer:
get_history(ticker, start_date, end_date=None, granularity=300)
Fetch/load historical data from Coinbase API at specified granularity
params:
start_date: (str) (see pandas.to_datetime for acceptable formats)
end_date: (str)
granularity: (int) seconds (default: 300)
We can plot this data immediately
from audiolizer.audiolizer import candlestick_plot, write_plot
fig = candlestick_plot(btc.loc['June 16, 2021'])
# write plot to disk so we can include it in markdown
write_plot(fig, 'plot_div_06-16-2021_300s.html')
Rebinning price history¶
The above resolution may be too high to search for patterns. Let's rebin it to a lower resolution.
from audiolizer.audiolizer import refactor
btc3h = refactor(btc.loc['June 16, 2021'], frequency='3h')
time | low | high | open | close | volume |
---|---|---|---|---|---|
2021-06-16 00:00:00 | 39612 | 40180.1 | 40158.1 | 40049.2 | 1175.11 |
2021-06-16 03:00:00 | 39833 | 40425 | 40046.7 | 40257.5 | 1329.77 |
2021-06-16 06:00:00 | 39930 | 40499 | 40261.8 | 40048.2 | 945.778 |
2021-06-16 09:00:00 | 38929.2 | 40187.1 | 40048.2 | 39098.9 | 1697.73 |
2021-06-16 12:00:00 | 38659.7 | 39518.1 | 39092.7 | 38854.5 | 2639.68 |
2021-06-16 15:00:00 | 38443 | 39310 | 38854.5 | 39180 | 2867.48 |
2021-06-16 18:00:00 | 38328.2 | 39706.6 | 39180 | 38533.5 | 4495.34 |
2021-06-16 21:00:00 | 38105 | 38927.9 | 38527.5 | 38351 | 2087.69 |
fig = candlestick_plot(btc3h)
# write plot to disk so we can include it in markdown
write_plot(fig, 'plot_div_06-16-2021_3h.html')
From price to frequency¶
Now that we have a more managable price history, we're ready to start audiolizing. We start with a simple linear map between price and frequency. The min/max price closing price is scaled to min and max pitches.
from audiolizer.audiolizer import frequencies, get_frequency
import numpy as np
C2_log = np.log10(frequencies['C2'])
C3_log = np.log10(frequencies['C3'])
btc3h['frequency'] = get_frequency(
btc3h.close.values,
btc3h.close.min(),
btc3h.close.values.max(),
[C2_log, C3_log])
# btc3h[['close', 'frequency']]
time | close | frequency |
---|---|---|
2021-06-16 00:00:00 | 40049.2 | 123.664 |
2021-06-16 03:00:00 | 40257.5 | 130.813 |
2021-06-16 06:00:00 | 40048.2 | 123.632 |
2021-06-16 09:00:00 | 39098.9 | 91.0648 |
2021-06-16 12:00:00 | 38854.5 | 82.6796 |
2021-06-16 15:00:00 | 39180 | 93.8467 |
2021-06-16 18:00:00 | 38533.5 | 71.6676 |
2021-06-16 21:00:00 | 38351 | 65.4064 |
From volume to amplitude¶
The amplitude of each beat will be set by volume, normalized by the maximum volume in the time range.
For the moment, the duration of each note will be fixed.
duration=.25 #sec
btc3h['amplitude']=btc3h.volume/btc3h.volume.max()
btc3h['duration'] = duration
btc3h[['close', 'frequency', 'volume', 'amplitude', 'duration']]
beeps = [(frequency,
amplitude,
duration) for frequency, amplitude, duration in btc3h[['frequency', 'amplitude', 'duration']].values]
Given our collection of beeps, we can now turn them into an audio file
from audiolizer.audiolizer import beeper, audiogen_p3, itertools
audio = [beeper(*beep) for beep in beeps]
with open('beeps_tonal.wav', "wb") as f:
audiogen_p3.sampler.write_wav(f, itertools.chain(*audio))
From tone to pitch¶
So far we have produced a sequence of pure tones. Let's turn them into pitches to make them a bit easier to hear. This has the effect of rebinning the price.
from audiolizer.audiolizer import pitch, freq
C4_log = np.log10(frequencies['C4'])
C5_log = np.log10(frequencies['C5'])
btc_hourly= refactor(btc.loc['June 16, 2021'].copy(), '1h')
btc_hourly['frequency'] = get_frequency(
btc_hourly.close.values,
btc_hourly.close.min(),
btc_hourly.close.values.max(),
[C4_log, C5_log])
btc_hourly['note'] = [pitch(_) for _ in btc_hourly.frequency]
btc_hourly['pitch'] = [freq(_) for _ in btc_hourly.note]
# print(btc_hourly[['close', 'frequency', 'note', 'pitch']].head().to_markdown())
time | close | frequency | note | pitch |
---|---|---|---|---|
2021-06-16 00:00:00 | 39886.9 | 458.764 | A#4 | 466.164 |
2021-06-16 01:00:00 | 40109.4 | 487.269 | B4 | 493.883 |
2021-06-16 02:00:00 | 40049.2 | 479.554 | A#4 | 466.164 |
2021-06-16 03:00:00 | 40026 | 476.591 | A#4 | 466.164 |
2021-06-16 04:00:00 | 40271.8 | 508.076 | B4 | 493.883 |
Duration¶
The note duration may be controlled by merging pitches that are the same based on volume. Here's our merging function:
def merge_pitches(beeps, amp_min):
merged = []
last_freq = 0
last_amp = 0
for freq, amp, dur in beeps:
if freq == last_freq:
if merged[-1][1] < amp_min:
merged[-1][1] = (amp + last_amp)/2 # todo: use moving average
merged[-1][2] += dur
continue
merged.append([freq, amp, dur])
last_freq = freq
last_amp = amp
return merged
As we cycle through the stack of beeps, we check if the last beep's frequency matches the current one. If so, we check if the last beep's amplitude is above the amp_min
threshold. If so, we set the last beep's amplitude to the average of the last and current and we increase the last beep's duration by the current beep's duration.
The point of doing this is that low amplitude indicates low (sideways) trading volume so we play a longer note, whereas high trade volume will get the minimum duration, and the melody will be "faster".
The quiet
function is similar:
def quiet(beeps, min_amp):
silenced = []
for freq, amp, dur in beeps:
if amp < min_amp:
amp = 0
silenced.append((freq, amp, dur))
return silenced
Here we just set the amplitude to 0 if it's below the min_amp
threshold.
fig = candlestick_plot(btc_hourly)
fig
write_plot(fig, 'plot_div_06-16-2021_1h.html')
max_vol = btc_hourly.volume.max()
beeps = [(pitch,
volume/max_vol,
duration) for pitch, volume in btc_hourly[['pitch', 'volume']].values]
beeps = quiet(merge_pitches(beeps, .25), .25)
audio = [beeper(*beep) for beep in beeps]
with open('beeps_pitch.wav', "wb") as f:
audiogen_p3.sampler.write_wav(f, itertools.chain(*audio))
The following sample is an audiolization of the above candlestick plot. The first 2.5 seconds are silent since the first half of the day's trades had relatively low volume.