Back to Community
Market Making's Troubling Tails

Hey all, this is a for fun example of what returns look like for liquidity providers in VXX. An interesting aspect of a structured product like VXX is that it's composed of futures, but doesn't expire. That means when a trade doesn't work out the trader gets a choice: exit the position, or add it to their inventory and wait. Taking the latter road exposes the trader to massive skew risk.

This is the short leg of a simplistic volatility market making strategy. It attempts to sell 1% of the account on every little pop in VXX, then cover the short after the trade has made a 2% return. A majority of the trades are closed for a profit within a short period of time (see the logs), but larger downward movements cause it to accumulate an inventory. Since it's selling risk premium with essentially no expiration the only unknown is the time it takes to earn a yield (also borrowing costs & forced covering).

This algo blew up in 2011, but that's skew risk for you.

Clone Algorithm
34
Loading...
Backtest from to with initial capital
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
Returns 1 Month 3 Month 6 Month 12 Month
Alpha 1 Month 3 Month 6 Month 12 Month
Beta 1 Month 3 Month 6 Month 12 Month
Sharpe 1 Month 3 Month 6 Month 12 Month
Sortino 1 Month 3 Month 6 Month 12 Month
Volatility 1 Month 3 Month 6 Month 12 Month
Max Drawdown 1 Month 3 Month 6 Month 12 Month
import pandas as pd



def initialize(context):

    context.salesman = InsuranceSalesmanFixedBets(
        symbol('VXX'),
        bet=-0.01,
        return_target=0.02,
        entry_signal=1.5,
        nobs=1000,
        max_bets=100,
        max_holding_days=365,
        bet_offset_minutes=15
    )
    schedule_function(record_vars, time_rule=time_rules.market_close())

    for minute in range(1, 390, 5):
        schedule_function(context.salesman.handle_data,
                          date_rule=date_rules.every_day(),
                          time_rule=time_rules.market_open(minutes=minute))

def record_vars(context, data):
    record(leverage=context.account.leverage,
           bet_count=len(context.salesman.bets))
    

class InsuranceSalesmanFixedBets():
    
    def __init__(self, vix,
                 bet=0.0,
                 return_target=0.10,
                 entry_signal=2.0,
                 nobs=1000,
                 max_bets=25,
                 max_holding_days=365,
                 bet_offset_minutes=1000):
        self.vix = vix
        self.bet = bet
        self.return_target = return_target
        self.entry_signal = entry_signal
        self.max_holding_days = max_holding_days
        self.nobs = nobs
        self.bets = []
        self.bet_offset = pd.Timedelta(minutes=bet_offset_minutes)
        self.max_bets = max_bets
        self.last_bet = pd.Timestamp('2009-01-01', tz='utc')

    def handle_data(self, context, data):
        self.handle_bets(context, data)

        if len(self.bets) >= self.max_bets:
            return

        now = get_datetime()
        if (now - self.bet_offset) <= self.last_bet:
            return

        signal = self.get_current_signal(data)

        if signal > self.entry_signal:
            bet = FixedBet(context, data, self.vix, pct=self.bet,
                           max_days=self.max_holding_days,
                           return_target=self.return_target)
            self.bets.append(bet)
            self.last_bet = now

    def get_current_signal(self, data):
        vix_prices = data.history(self.vix, 'price', self.nobs, '1m')
        return (vix_prices.iloc[-1] - vix_prices.mean()) / vix_prices.std()

    def handle_bets(self, context, data):
        for bet in iter(self.bets):
            bet.update(context, data)
            if bet.closed:
                self.bets.remove(bet)
    
    
class FixedBet(object):

    def __init__(self, context, data, asset,
                 pct=-0.01, max_days=300, return_target=0.10):

        self.asset = asset
        pval = context.portfolio.portfolio_value
        value_target = pct * pval
        self.px = data.current(self.asset, 'price')
        self.shares = value_target // self.px
        self._start = get_datetime()
        self.return_target = return_target
        self.date_target = self._start + pd.Timedelta(days=max_days)
        self._order = order(self.asset, self.shares,
                            style=LimitOrder(self.px - 0.02))
        self.closed = False

    def update(self, context, data):
        now = get_datetime()
        order_obj = get_order(self._order)
        px = order_obj.limit
        R = px / data.current(self.asset, 'price') - 1
        delta = now - self._start
        if R > self.return_target:
            log.info("[{}] Profitable Trade R={}, {}".format(
                    self.asset.symbol, R, delta))
            order_amount = -order_obj.filled
            order(self.asset, order_amount)
            self.closed = True
            return
        if now < self.date_target:
            return
        order_amount = -order_obj.filled
        order(self.asset, order_amount)
        log.info("[{}] Expired trade: R={}, time={}".format(
                self.asset.symbol, R, delta))
        self.closed = True

        
def handle_data(context, data):
    pass

There was a runtime error.
5 responses

As a market maker, the algo should not accumulate into a position (yours blew up in 2011 because of it), but trade in and out of it.

As such, you need a stop loss order, and it needs to be linked to your profit taking order. You also need a linked time stop, so that is no colsing orders get triggered and filled, the algo drops the security at a certain point after it bought it. From what Q IT has said, this is not possible to do, so if you can elaborate/edit/update the code, that would be great.

Hey BT. Thanks for the feedback, most of the things you mentioned are actually already implemented in the algo. It trades in and out, averages 10 transactions per day and as many as 46. It has a time linked stop on each trade, it's set high to demonstrate what happens when inventory is allowed to accumulate. The only thing not there is the stop orders, but that's on purpose because of the nature of what it's trading.

This was just a for fun example of what skew risk looks like

Thanks David,

Can you add the stop loss and link it to the profit taking order as well as the time linked closing order, for each bet, in "one cancels all" fashion?

Thanks,
BT

I can't submit the orders as an OCA group if that's what you mean. As I'm sure you know, Q doesn't yet support a lot of the more complex order types.

What you can do is simulate those types of orders by building them in your algo. Of course you would be constrained to updating them once per minute, so they would need to be wide enough apart so more than one filling in a single bar is unlikely. It wouldn't be perfect, but it is possible.

In my backtests, I noticed a few instances in the log where Q warned of the automatic canceling of limit orders at end of day.

Maybe I missed it, but is there logic to re-establish those limit orders at the start of the next day?

Am just wondering if this could be a factor contributing to the accumulation that is mentioned above.

Richard
http://quant-coder.prokopyshen.com/ContactMe