Backtesting MXC Liquidation Strategies

Objective of this anlysis is to explore potential ways of liquidating a cryptocurrency called MXC given fixed daily revenues from miner(s) producing more MXC.

import datetime as dt
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np

plt.rcParams.update({'figure.max_open_warning': 0})

The data is taken from a coincodex.com export of MXC pricing, from here.

Open Raw Data

p = 'backtesting-mxc-liquidation-strategies/mxc_2018-12-7_2021-11-8.csv'
d = pd.read_csv(p)
d.info()
d.head()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1068 entries, 0 to 1067
Data columns (total 7 columns):
 #   Column      Non-Null Count  Dtype  
---  ------      --------------  -----  
 0   Date        1068 non-null   object 
 1   Open        1068 non-null   float64
 2   High        1068 non-null   float64
 3   Low         1068 non-null   float64
 4   Close       1068 non-null   float64
 5   Volume      1068 non-null   float64
 6   Market Cap  1068 non-null   float64
dtypes: float64(6), object(1)
memory usage: 58.5+ KB

Date Open High Low Close Volume Market Cap
0 Nov-08-2021 0.050362 0.056709 0.049586 0.051690 14782927.77 130838993.6
1 Nov-07-2021 0.052592 0.052654 0.050334 0.050362 13311073.03 132167070.3
2 Nov-06-2021 0.052126 0.052930 0.049906 0.052592 15592909.12 132933849.2
3 Nov-05-2021 0.053902 0.055087 0.052059 0.052126 12949702.98 137597706.6
4 Nov-04-2021 0.054830 0.056718 0.053049 0.053902 14277470.17 141224939.6

Basic Cleansing

d.columns = d.columns.str.lower()
d['date'] = pd.to_datetime(d['date'])
d['yy-mm'] = (d['date'].dt.year.astype(str).str[2:]
              + '-' +
              d['date'].dt.month.astype(str).str.zfill(2))
d = (d[['date','yy-mm','close']]
     [(d['date'].dt.year>=2019)&
      (~((d['date'].dt.year==2021)&
         (d['date'].dt.month==11)))]
     .sort_values(['date'])
     .reset_index(drop=True))
d.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1035 entries, 0 to 1034
Data columns (total 3 columns):
 #   Column  Non-Null Count  Dtype         
---  ------  --------------  -----         
 0   date    1035 non-null   datetime64[ns]
 1   yy-mm   1035 non-null   object        
 2   close   1035 non-null   float64       
dtypes: datetime64[ns](1), float64(1), object(1)
memory usage: 24.4+ KB
print('\nDate Range: ' + str(d['date'].min())[:10] + ' - ' + str(d['date'].max())[:10])
d.head()
Date Range: 2019-01-01 - 2021-10-31

date yy-mm close
0 2019-01-01 19-01 0.005785
1 2019-01-02 19-01 0.005799
2 2019-01-03 19-01 0.006077
3 2019-01-04 19-01 0.012143
4 2019-01-05 19-01 0.013452

Histograms

monthly_starting_mxc = 6000
daily_mined_mxc = 21.2
c = d.drop(columns=['yy-mm'])
summary = pd.DataFrame()
for start_row in np.arange(d.shape[0]-30):
    sub = c[start_row:start_row + 30].reset_index(drop=True)

    sub['open'] = sub.loc[0]['close']
    sub = sub[['date','open','close']].copy()
    sub['base_dollars'] = monthly_starting_mxc
    sub['base_mxc'] = 0
    sub['base_mxc'] = monthly_starting_mxc / sub.loc[0]['close']
    sub['mined_mxc'] = daily_mined_mxc / sub['close']
    sub['cum_mined_mxc'] = sub['mined_mxc'].cumsum()
    sub = sub.drop(columns=['mined_mxc'])
    sub['total_mxc'] = sub['base_mxc'] + sub['cum_mined_mxc']
    sub['total_dollars'] = sub['total_mxc'] * sub['close']
    sub['month_end_delta'] = sub['total_dollars'] - monthly_starting_mxc
    
    new_summary_row = pd.DataFrame(sub.loc[29]).T
    summary = summary.append(new_summary_row)
summary = summary.reset_index(drop=True)
summary.head()

date open close base_dollars base_mxc cum_mined_mxc total_mxc total_dollars month_end_delta
0 2019-01-30 0.005785 0.008925 6000 1037122.77252 61484.710737 1098607.483256 9805.004773 3805.004773
1 2019-01-31 0.005799 0.008539 6000 1034725.562326 60302.966827 1095028.529153 9350.334727 3350.334727
2 2019-02-01 0.006077 0.008676 6000 987330.086663 59090.349032 1046420.435695 9079.151804 3079.151804
3 2019-02-02 0.012143 0.008916 6000 494131.122305 57979.500993 552110.623299 4922.679602 -1077.320398
4 2019-02-03 0.013452 0.009283 6000 446021.311493 58517.369139 504538.680632 4683.522583 -1316.477417
plt.figure(figsize=(16,9))
bins=np.arange(-5000,17000,250)
plt.hist(summary['month_end_delta'],
         bins=bins,
         alpha=0.8)
plt.axvline(summary['month_end_delta'].quantile(.25), c='C1', alpha=0.8, zorder=0)
plt.axvline(summary['month_end_delta'].quantile(.50), c='C1', alpha=0.8, zorder=0)
plt.axvline(summary['month_end_delta'].quantile(.75), c='C1', alpha=0.8, zorder=0)
plt.gca().set_ylim([0,225])
plt.grid()
for spine in plt.gca().spines.values():
    spine.set_visible(False)
plt.gca().tick_params(
        axis='x',
        bottom=False)
plt.gca().tick_params(
    axis='y',
    left=False,
    right=False)

plt.ylabel('# 30-Day Periods')
plt.xlabel('30-Day Income (Delta From 30-Day Starting Balance: ${})'.format(monthly_starting_mxc))
plt.title('''30-Day MXC Income Assuming Each 30-Day Period Starts with MXC Balance worth \${}
Daily MXC Income worth \${:2.2f}, Backtesting 30-Day Periods over 2019-01-01 thru 2021-10-31
{}% ({}/{}) / {}% ({}/{}) Periods with no Income / \$3K+ Income
25th, 50th, 75th Percentile End-of-Period Income = \${}, \${}, \${}'''
              .format(monthly_starting_mxc, daily_mined_mxc,
                      int(summary[summary['month_end_delta']<0].shape[0]/summary.shape[0]*100.),
                      summary[summary['month_end_delta']<0].shape[0], summary.shape[0],
                      int(summary[summary['month_end_delta']>=3000].shape[0]/summary.shape[0]*100.),
                      summary[summary['month_end_delta']>=3000].shape[0], summary.shape[0],
                      int(round(summary['month_end_delta'].quantile(.25))),
                      int(round(summary['month_end_delta'].quantile(.50))),
                      int(round(summary['month_end_delta'].quantile(.75)))))
rects = plt.gca().patches
for rect in rects:
    if rect.get_x() < 0:
        rect.set_color('C3')
    if rect.get_x() >= 0:
        rect.set_color('C0')

png

Parametrize and Run with Various Inputs

for monthly_starting_mxc in np.arange(1000,7000,1000):
    daily_mined_mxc = 21.2
    summary = pd.DataFrame()
    for start_row in np.arange(d.shape[0]-30):
        sub = c[start_row:start_row + 30].reset_index(drop=True)

        sub['open'] = sub.loc[0]['close']
        sub = sub[['date','open','close']].copy()
        sub['base_dollars'] = monthly_starting_mxc
        sub['base_mxc'] = 0
        sub['base_mxc'] = monthly_starting_mxc / sub.loc[0]['close']
        sub['mined_mxc'] = daily_mined_mxc / sub['close']
        sub['cum_mined_mxc'] = sub['mined_mxc'].cumsum()
        sub = sub.drop(columns=['mined_mxc'])
        sub['total_mxc'] = sub['base_mxc'] + sub['cum_mined_mxc']
        sub['total_dollars'] = sub['total_mxc'] * sub['close']
        sub['month_end_delta'] = sub['total_dollars'] - monthly_starting_mxc

        new_summary_row = pd.DataFrame(sub.loc[29]).T
        summary = summary.append(new_summary_row)
    summary = summary.reset_index(drop=True)

    plt.figure(figsize=(16,9))
    bins=np.arange(-5000,15000,250)
    plt.hist(summary['month_end_delta'],
             bins=bins,
             alpha=0.8)
    plt.axvline(summary['month_end_delta'].quantile(.25), c='C1', alpha=0.8, zorder=0)
    plt.axvline(summary['month_end_delta'].quantile(.50), c='C1', alpha=0.8, zorder=0)
    plt.axvline(summary['month_end_delta'].quantile(.75), c='C1', alpha=0.8, zorder=0)
    plt.gca().set_ylim([0,225])
    plt.grid()
    for spine in plt.gca().spines.values():
        spine.set_visible(False)
    plt.gca().tick_params(
            axis='x',
            bottom=False)
    plt.gca().tick_params(
        axis='y',
        left=False,
        right=False)

    plt.ylabel('# 30-Day Periods')
    plt.xlabel('30-Day Income (Delta From 30-Day Starting Balance: ${})'.format(monthly_starting_mxc))
    plt.title('''30-Day MXC Income Assuming Each 30-Day Period Starts with MXC Balance worth \${}
    Daily MXC Income worth \${:2.2f}, Backtesting 30-Day Periods over 2019-01-01 thru 2021-10-31
    {}% ({}/{}) / {}% ({}/{}) Periods with no Income / \$3K+ Income
    25th, 50th, 75th Percentile End-of-Period Income = \${}, \${}, \${}'''
                  .format(monthly_starting_mxc, daily_mined_mxc,
                          int(summary[summary['month_end_delta']<0].shape[0]/summary.shape[0]*100.),
                          summary[summary['month_end_delta']<0].shape[0], summary.shape[0],
                          int(summary[summary['month_end_delta']>=3000].shape[0]/summary.shape[0]*100.),
                          summary[summary['month_end_delta']>=3000].shape[0], summary.shape[0],
                          int(round(summary['month_end_delta'].quantile(.25))),
                          int(round(summary['month_end_delta'].quantile(.50))),
                          int(round(summary['month_end_delta'].quantile(.75)))))
    rects = plt.gca().patches
    for rect in rects:
        if rect.get_x() < 0:
            rect.set_color('C3')
        if rect.get_x() >= 0:
            rect.set_color('C0')

    plt.savefig('backtesting-mxc-liquidation-strategies/figures/final_hists_'+str(monthly_starting_mxc)+'.pdf',
                dpi=450)

png

png

png

png

png

png


Line Graphs

Math on Purchases

monthly_starting_mxc = 5000
daily_mined_mxc = 21.2

stack = pd.DataFrame()
for yymm in d['yy-mm'].unique():
    sub = d[d['yy-mm']==yymm].reset_index(drop=True)
    sub.loc[sub['date'].dt.day==1, 'start/end'] = 'Start'
    sub.loc[sub.shape[0]-1, 'start/end'] = 'End'
    
    sub.loc[sub['start/end']=='Start', 'start_mxc'] = monthly_starting_mxc / sub['close']
    sub['daily_mxc'] = (daily_mined_mxc / sub['close']) + sub['start_mxc'].fillna(0)
    sub['cum_mxc'] = sub['daily_mxc'].cumsum()
    sub['start_dollars'] = sub['start_mxc'].fillna(method='ffill') * sub['close']
    sub['cum_dollars'] = sub['cum_mxc'] * sub['close']
    
    sub.loc[sub['start/end']=='End', 'month_end_delta'] = monthly_starting_mxc
    sub['month_end_delta'] = sub['cum_dollars'] - sub['month_end_delta']
    
    stack = (stack.append(sub).reset_index(drop=True))
stack.head(32)

date yy-mm close start/end start_mxc daily_mxc cum_mxc start_dollars cum_dollars month_end_delta
0 2019-01-01 19-01 0.005785 Start 864268.977100 867933.477563 867933.477563 5000.000000 5021.200000 NaN
1 2019-01-02 19-01 0.005799 NaN NaN 3656.030320 871589.507883 5011.583797 5054.032912 NaN
2 2019-01-03 19-01 0.006077 NaN NaN 3488.566306 875078.074189 5252.158252 5317.845081 NaN
3 2019-01-04 19-01 0.012143 NaN NaN 1745.929965 876824.004154 10494.408525 10646.858268 NaN
4 2019-01-05 19-01 0.013452 NaN NaN 1575.941967 878399.946122 11626.381361 11816.475000 NaN
5 2019-01-06 19-01 0.012000 NaN NaN 1766.682272 880166.628394 10371.136113 10561.906243 NaN
6 2019-01-07 19-01 0.013553 NaN NaN 1564.268103 881730.896497 11713.147052 11949.802579 NaN
7 2019-01-08 19-01 0.017322 NaN NaN 1223.908169 882954.804665 14970.487807 15294.155509 NaN
8 2019-01-09 19-01 0.017328 NaN NaN 1223.419269 884178.223934 14976.470277 15321.467322 NaN
9 2019-01-10 19-01 0.013635 NaN NaN 1554.791703 885733.015637 11784.538263 12077.206159 NaN
10 2019-01-11 19-01 0.012994 NaN NaN 1631.584647 887364.600284 11229.881547 11529.974596 NaN
11 2019-01-12 19-01 0.011411 NaN NaN 1857.809891 889222.410175 9862.420479 10147.171240 NaN
12 2019-01-13 19-01 0.012252 NaN NaN 1730.276783 890952.686958 10589.347608 10916.286428 NaN
13 2019-01-14 19-01 0.012396 NaN NaN 1710.254768 892662.941726 10713.317486 11065.283790 NaN
14 2019-01-15 19-01 0.011887 NaN NaN 1783.390410 894446.332136 10273.971537 10632.703940 NaN
15 2019-01-16 19-01 0.011497 NaN NaN 1843.949029 896290.281165 9936.555743 10304.706725 NaN
16 2019-01-17 19-01 0.011161 NaN NaN 1899.446867 898189.728032 9646.230508 10024.824894 NaN
17 2019-01-18 19-01 0.011536 NaN NaN 1837.681574 900027.409606 9970.444594 10382.963705 NaN
18 2019-01-19 19-01 0.011301 NaN NaN 1876.001105 901903.410711 9766.786523 10192.079446 NaN
19 2019-01-20 19-01 0.010902 NaN NaN 1944.640845 903848.051556 9422.049507 9853.530919 NaN
20 2019-01-21 19-01 0.010531 NaN NaN 2013.177577 905861.229133 9101.284719 9539.276753 NaN
21 2019-01-22 19-01 0.009855 NaN NaN 2151.231362 908012.460495 8517.216065 8948.300264 NaN
22 2019-01-23 19-01 0.010282 NaN NaN 2061.902395 910074.362890 8886.212248 9357.172552 NaN
23 2019-01-24 19-01 0.010355 NaN NaN 2047.411483 912121.774372 8949.105966 9444.599573 NaN
24 2019-01-25 19-01 0.010267 NaN NaN 2064.863800 914186.638173 8873.467738 9385.973412 NaN
25 2019-01-26 19-01 0.009891 NaN NaN 2143.269477 916329.907649 8548.856088 9063.813138 NaN
26 2019-01-27 19-01 0.009396 NaN NaN 2256.243729 918586.151378 8120.799221 8631.171429 NaN
27 2019-01-28 19-01 0.008512 NaN NaN 2490.684605 921076.835983 7356.412081 7839.944442 NaN
28 2019-01-29 19-01 0.009211 NaN NaN 2301.485479 923378.321461 7961.163555 8505.645852 NaN
29 2019-01-30 19-01 0.008925 NaN NaN 2375.366375 925753.687837 7713.547900 8262.295193 NaN
30 2019-01-31 19-01 0.008539 End NaN 2482.756553 928236.444390 7379.902911 7926.114462 2926.114462
31 2019-02-01 19-02 0.008676 Start 576276.538975 578719.951501 578719.951501 5000.000000 5021.200000 NaN

Add Data for Price Bands

stack['c_low_price'] = stack['close'].rolling(30).min()
stack['c_high_price'] = stack['close'].rolling(30).max()
stack['c_low_usd'] = stack['cum_mxc'] * stack['c_low_price']
stack['c_high_usd'] = stack['cum_mxc'] * stack['c_high_price']
sub = stack[stack['yy-mm']=='19-01'].reset_index(drop=True)

plt.figure(figsize=(16,9))

ax1 = plt.gca()
lns1 = ax1.plot(sub['date'], sub['cum_dollars'], 
                label='Value of Total (Starting + Daily Purchased) MXC (Dollars)')
lns2 = ax1.plot(sub['date'], sub['start_mxc'].fillna(method='ffill')*sub['close'],
                label='Value of Starting MXC (Dollars)')

ax1.set_title('Daily Dollar Value of Purchases and Retained MXC')
ax1.set_xlabel('Date')
ax1.set_ylabel('Dollars')
ax1.grid()
ax1.set_ylim([-100,18100])

ax2 = ax1.twinx()
lns3 = ax2.bar(sub['date'], sub['daily_mxc']-sub['start_mxc'].fillna(0), color='C2', alpha=0.125,
               label='Purchased MXC (MXC)')
ax2.set_ylim([-100,18100])
ax2.set_ylabel('MXC')

lns4 = ax1.fill_between(sub['date'], sub['c_low_usd'], sub['c_high_usd'], alpha=0.1,
                        label='30-Day High/Low Price Band')

for ax in [ax1, ax2]:
    for spine in ax.spines.values():
        spine.set_visible(False);
    ax.tick_params(
        axis='y',
        left=False,
        right=False)
    ax.tick_params(
        axis='x',
        bottom=False)

lines = lns1+lns2+[lns3]+[lns4]
labels = [l.get_label() for l in lines]
ax.legend(lines, labels, loc=2);

png

Parametrize and Run Over All Months in Dataset

for yymm in stack['yy-mm'].unique():
    sub = stack[stack['yy-mm']==yymm].reset_index(drop=True)

    plt.figure(figsize=(16,9))

    ax1 = plt.gca()
    lns1 = ax1.plot(sub['date'], sub['cum_dollars'], 
                    label='Value of Total (Starting + Daily Purchased) MXC (Dollars)')
    lns2 = ax1.plot(sub['date'], sub['start_mxc'].fillna(method='ffill')*sub['close'],
                    label='Value of Starting MXC (Dollars)')

    ax1.set_title('''Daily Dollar Value of Purchases and Retained MXC During {}
Assuming Each Month Starts with MXC Balance worth \${}
Daily MXC Income worth \${:2.2f}
Starting / Ending MXC Price \${:1.3f} / \${:1.3f}'''.format(yymm,
                                                            monthly_starting_mxc,
                                                            daily_mined_mxc,
                                                            sub.iloc[0]['close'],
                                                            sub.iloc[-1]['close']))
    
    ax1.set_xlabel('Date')
    ax1.set_ylabel('Dollars')
    ax1.grid()
    ax1.set_ylim([-100,18100])

    ax2 = ax1.twinx()
    lns3 = ax2.bar(sub['date'], sub['daily_mxc']-sub['start_mxc'].fillna(0), color='C2', alpha=0.125,
                   label='Purchased MXC (MXC)')
    ax2.set_ylim([-100,18100])
    ax2.set_ylabel('MXC')
    
    lns4 = ax1.fill_between(sub['date'], sub['c_low_usd'], sub['c_high_usd'], alpha=0.1,
                            label='30-Day High/Low Price Band')

    for ax in [ax1, ax2]:
        for spine in ax.spines.values():
            spine.set_visible(False);
        ax.tick_params(
            axis='y',
            left=False,
            right=False)
        ax.tick_params(
            axis='x',
            bottom=False)

    lines = lns1+lns2+[lns3]+[lns4]
    labels = [l.get_label() for l in lines]
    ax.legend(lines, labels, loc=2);
    
    plt.savefig('backtesting-mxc-liquidation-strategies/figures/lines_' + yymm + '.pdf',
                dpi=450)

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png

png