def algo_loop(trading_day, tick_coef, peg_to_bbo): log_message('Beginning Market-Making Strategy run') round_lot = 100 avg_spread = (trading_day.ask_px - trading_day.bid_px).mean() half_spread = avg_spread / 2 print("Average stock spread for sample: {:.4f}".format(avg_spread)) # this should be a higher hard constraint but for now let's just leave it at a fixed amount live_order_quantity = 200 # init our price and volume variables [last_price, last_size, bid_price, bid_size, ask_price, ask_size, volume] = np.zeros(7) # init our counters [trade_count, quote_count, cumulative_volume] = [0, 0, 0] # init some time series objects for collection of telemetry fair_values = pd.Series(index=trading_day.index) midpoints = pd.Series(index=trading_day.index) tick_factors = pd.Series(index=trading_day.index) # let's set up a container to hold trades. preinitialize with the index buys = pd.DataFrame(columns=['price', 'shares', 'bar', 'trade_type'], index=trading_day.index) sells = pd.DataFrame(columns=['price', 'shares', 'bar', 'trade_type'], index=trading_day.index) # leaving this as a DataFrame since I may want to add other attributes net_positions = pd.DataFrame(columns=['position'], index=trading_day.index) current_net_pos = 0 # MAIN EVENT LOOP current_bar = 0 # other order and market variables total_quantity_bought = 0 total_quantity_sold = 0 vwap_numerator = 0.0 total_trade_count = 0 total_agg_count = 0 total_pass_count = 0 # fair value pricing variables midpoint = 0.0 fair_value = 0.0 # define our accumulator for the tick EMA message_type = 0 tick_window = 20 tick_factor = 0 tick_ema_alpha = 2 / (tick_window + 1) prev_tick = 0 prev_price = 0 # quoting related our_bid = 0.0 our_ask = 0.0 half_spread_coef = 0.5 # risk factor for part 2 risk_factor = 0.0 risk_coef = 0.0 log_message('starting main loop') for index, row in trading_day.iterrows(): # get the time of this message time_from_open = (index - pd.Timedelta(hours=9, minutes=30)) minutes_from_open = (time_from_open.hour * 60) + time_from_open.minute # MARKET DATA HANDLING if pd.isna(row.trade_px): # it's a quote # skip if not NBBO if not ((row.qu_source == 'N') and (row.natbbo_ind == 4)): continue # set our local NBBO variables if (row.bid_px > 0 and row.bid_size > 0): bid_price = row.bid_px bid_size = row.bid_size * round_lot if (row.ask_px > 0 and row.ask_size > 0): ask_price = row.ask_px ask_size = row.ask_size * round_lot quote_count += 1 message_type = 'q' else: # it's a trade # store the last trade price prev_price = last_price # now get the new data last_price = row.trade_px last_size = row.trade_size trade_count += 1 cumulative_volume += row.trade_size vwap_numerator += last_size * last_price message_type = 't' # check the trade against our quotes # for now assume we always have a quote out there # has our quote been hit or lifted? # check bid if (last_price <= our_bid) & (bid_price > 0): # our bid was hit fill_size = min(live_order_quantity, last_size) record_trade(buys, index, our_bid, fill_size, current_bar, 'p', 'b') current_net_pos += fill_size net_positions.loc[index] = [current_net_pos] #print("buy: NBO: {} trade size: {} net_pos: {}".format( our_bid, fill_size, current_net_pos ) ) # check offer if (last_price >= our_ask) & (ask_price > 0): # our ask was lifted fill_size = min(live_order_quantity, last_size) record_trade(sells, index, our_ask, fill_size, current_bar, 'p', 's') current_net_pos -= fill_size net_positions.loc[index] = [current_net_pos] #print("sell: NBO: {} trade size: {} net_pos: {}".format( our_ask, fill_size, current_net_pos ) ) # TICK FACTOR # only update if it's a trade if message_type == 't': # calc the tick this_tick = np.sign(last_price - prev_price) if this_tick == 0: this_tick = prev_tick # now calc the tick if tick_factor == 0: tick_factor = this_tick else: tick_factor = (tick_ema_alpha * this_tick) + (1 - tick_ema_alpha) * tick_factor # store the last tick prev_tick = this_tick # RISK FACTOR # TODO # PRICING LOGIC new_midpoint = bid_price + (ask_price - bid_price) / 2 if new_midpoint > 0: midpoint = new_midpoint # FAIR VALUE CALCULATION # check inputs, skip of the midpoint is zero, we've got bogus data (or we're at start of day) if midpoint == 0: #print( "{} no midpoint. b:{} a:{}".format( index, bid_price, ask_price ) ) continue fair_value = midpoint + half_spread * ((tick_coef * tick_factor) + (risk_coef * risk_factor)) # collect our data fair_values[index] = fair_value midpoints[index] = midpoint tick_factors[index] = tick_factor # QUOTE PLACEMENT # for now we're going to assume no hedging or aggressive trading at all. # in this version just ignore fair value and always set our price to the BBO if peg_to_bbo: #print("Pegging to NBBO") our_bid = bid_price our_ask = ask_price # use our fair value else: # in this version, bid and offer are placed around the fair value but constrained by nbbo our_bid = round( min(fair_value - (half_spread_coef * half_spread), bid_price), 2) our_ask = round( max(fair_value + (half_spread_coef * half_spread), ask_price), 2) #print("bid: {} ask: {}".format(our_bid, our_ask)) # looping done log_message('end simulation loop') log_message('order analytics') # TODO: add a final trade for net position at m2m to do simple P&L closeout if current_net_pos > 0: # we need to sell record_trade(sells, index, bid_price, current_net_pos, current_bar, 'p', 's') print("selling to close residual of {} shares".format(current_net_pos)) current_net_pos -= current_net_pos else: # we need to buy record_trade(buys, index, ask_price, current_net_pos, current_bar, 'p', 'b') print("buying to close residual of {} shares".format(current_net_pos)) current_net_pos += current_net_pos # Now, let's look at some stats # clean up all the ticks in which we didn't do trades buys = buys.dropna() sells = sells.dropna() log_message('Algo run complete.') # assemble results and return # TODO: add P&L return { 'midpoints': midpoints, 'fair_values': fair_values, 'tick_factors': tick_factors, 'buys': buys, 'sells': sells, 'quote_count': quote_count, 'net_positions': net_positions, 'residual_position': current_net_pos }
def algo_loop(trading_day, order_side, original_order_quantity, vwap_coefficients): log_message('Beginning VWAP run: {:s} {:d} shares'.format( order_side, original_order_quantity)) round_lot = 100 avg_spread = (trading_day.ask_px - trading_day.bid_px).mean() print("Average stock spread for sample: {:.4f}".format(avg_spread)) # generate target schedule - use bins 1 - 390 giving an automatic 1 minute "look ahead" # note that targets have been converted to shares from percent order_targets = vwap_target(np.arange(0, 390, dtype='int64'), vwap_coefficients) * original_order_quantity # let's force the last bin to complete order_targets[-1] = original_order_quantity # init our price and volume variables [last_price, last_size, bid_price, bid_size, ask_price, ask_size, volume] = np.zeros(7) # init our counters [trade_count, quote_count, cumulative_volume] = [0, 0, 0] # init some time series objects for collection of telemetry fair_values = pd.Series(index=trading_day.index) midpoints = pd.Series(index=trading_day.index) schedule_factors = pd.Series(index=trading_day.index) # let's set up a container to hold trades. preinitialize with the index trades = pd.DataFrame(columns=['price', 'shares', 'bar', 'trade_type'], index=trading_day.index) max_behind = 2000 # MAIN EVENT LOOP current_bar = 0 current_target_shares = 0 quantity_behind = 0 # track state and values for a current working order live_order = False live_order_price = 0.0 live_order_quantity = 0.0 # other order and market variables total_quantity_filled = 0 quantity_remaining = original_order_quantity - total_quantity_filled vwap_numerator = 0.0 total_trade_count = 0 total_agg_count = 0 total_pass_count = 0 # fair value pricing variables midpoint = 0.0 fair_value = 0.0 schedule_factor = 0.0 schedule_coef = 1.0 log_message('starting main loop') for index, row in trading_day.iterrows(): # get the time of this message time_from_open = (index - pd.Timedelta(hours=9, minutes=30)) minutes_from_open = (time_from_open.hour * 60) + time_from_open.minute # MARKET DATA HANDLING if pd.isna(row.trade_px): # it's a quote # skip if not NBBO if not ((row.qu_source == 'N') and (row.natbbo_ind == 4)): continue # set our local NBBO variables if (row.bid_px > 0 and row.bid_size > 0): bid_price = row.bid_px bid_size = row.bid_size * round_lot if (row.ask_px > 0 and row.ask_size > 0): ask_price = row.ask_px ask_size = row.ask_size * round_lot quote_count += 1 else: # it's a trade last_price = row.trade_px last_size = row.trade_size trade_count += 1 cumulative_volume += row.trade_size vwap_numerator += last_size * last_price # CHECK OPEN ORDER(S) if we have a live order, # has it been filled by the trade that just happened? if live_order: if (order_side == 'b') and (last_price <= live_order_price): fill_size = min(live_order_quantity, last_size) record_trade(trades, index, live_order_price, fill_size, current_bar, 'p') total_quantity_filled += fill_size total_pass_count += 1 # even if we only got partially filled, let's assume we're cancelling the entire quantity. # If we're still behind we'll replace later in the loop live_order = False live_order_price = 0.0 live_order_quantity = 0.0 quantity_behind = current_target_shares - total_quantity_filled if (order_side == 's') and (last_price >= live_order_price): fill_size = min(live_order_quantity, last_size) record_trade(trades, index, live_order_price, fill_size, current_bar, 'p') total_quantity_filled += fill_size total_pass_count += 1 # even if we only got partially filled, let's assume we're cancelling the entire quantity. # If we're still behind we'll replace later in the loop live_order = False live_order_price = 0.0 live_order_quantity = 0.0 quantity_behind = current_target_shares - total_quantity_filled # SCHEDULE FACTOR if minutes_from_open > current_bar: # we're in a new bar do new bar things here current_bar = minutes_from_open current_target_shares = order_targets[current_bar] quantity_behind = current_target_shares - total_quantity_filled # uncomment for debug #print( "{} bar: {:d} target:{:.2f}".format(index, current_bar, current_target_shares) ) # PRICING LOGIC new_midpoint = bid_price + (ask_price - bid_price) / 2 if new_midpoint > 0: midpoint = new_midpoint schedule_factor = calc_schedule_factor(max_behind, current_target_shares, total_quantity_filled, original_order_quantity) # FAIR VALUE CALCULATION # check inputs, skip of the midpoint is zero, we've got bogus data (or we're at start of day) if midpoint == 0: #print( "{} no midpoint. b:{} a:{}".format( index, bid_price, ask_price ) ) continue fair_value = midpoint + (schedule_coef * schedule_factor * avg_spread / 2) # collect our data fair_values[index] = fair_value midpoints[index] = midpoint schedule_factors[index] = schedule_factor # TRADING LOGIC # check where our FV is versus the BBO and constrain if order_side == 'b': if fair_value >= ask_price and quantity_behind > round_lot: total_agg_count += 1 new_trade_price = ask_price # now place our aggressive order: assume you can execute the full size across spread new_order_quantity = calc_order_quantity( quantity_behind, round_lot, quantity_remaining) record_trade(trades, index, new_trade_price, new_order_quantity, current_bar, 'a') # update quantity remaining quantity_remaining = min( 0, quantity_remaining - new_order_quantity) total_quantity_filled += new_order_quantity live_order_quantity = 0.0 live_order_price = 0.0 live_order = False else: # we're not yet willing to cross the spread, stay passive if quantity_behind > round_lot: live_order_price = bid_price live_order_quantity = calc_order_quantity( quantity_behind, round_lot, quantity_remaining) #live_order_quantity = quantity_behind live_order = True elif order_side == 's': if fair_value <= bid_price and quantity_behind > round_lot: total_agg_count += 1 new_trade_price = bid_price # now place our aggressive order: assume you can execute the full size across spread new_order_quantity = calc_order_quantity( quantity_behind, round_lot, quantity_remaining) #new_order_quantity = quantity_behind record_trade(trades, index, new_trade_price, new_order_quantity, current_bar, 'a') # update quantity remaining quantity_remaining = min( 0, quantity_remaining - new_order_quantity) total_quantity_filled += new_order_quantity live_order_quantity = 0.0 live_order_price = 0.0 live_order = False else: # not yet willing to cross spread if quantity_behind > round_lot: live_order_price = ask_price live_order_quantity = calc_order_quantity( quantity_behind, round_lot, quantity_remaining) #live_order_quantity = quantity_behind live_order = True else: # we shouldn't have got here, something is wrong with our order type print('Got an unexpected order_side value: ' + str(order_side)) # looping done log_message('end simulation loop') log_message('order analytics') # Now, let's look at some stats trades = trades.dropna() day_vwap = vwap_numerator / cumulative_volume # prep our text output avg_price = (trades['price'] * trades['shares']).sum() / trades['shares'].sum() log_message('VWAP run complete.') # assemble results and return return { 'midpoints': midpoints, 'fair_values': fair_values, 'schedule_factors': schedule_factors, 'trades': trades, 'quote_count': quote_count, 'day_vwap': day_vwap, 'avg_price': avg_price, 'order_targets': order_targets }
def algo_loop( trading_day, risk_adj = 0, risk_denominator=1, tick_coef = 1, tick_window = 20, spread_factor = 2, risk_factor=0): log_message( 'Beginning Tick Strategy run' ) #log_message( 'TODO: remove this message. Simply a test to see how closely you are reading this code' ) round_lot = 100 avg_spread = (trading_day.ask_px - trading_day.bid_px).mean() print("Average stock spread for sample: {:.4f}".format(avg_spread)) # init our price and volume variables [last_price, last_size, bid_price, bid_size, ask_price, ask_size, volume] = np.zeros(7) # init some time series objects for collection of telemetry fair_values = pd.Series(index=trading_day.index) midpoints = pd.Series(index=trading_day.index) # tick_factors = pd.Series( index=trading_day.index ) # risk_factors = pd.Series( index=trading_day.index ) # let's set up a container to hold trades. preinitialize with the index trades = pd.DataFrame( columns=['tick', 'risk', 'fair_value', 'market_price', 'trade_price', 'avg_price', 'position', 'unrealized_pnl', 'realized_pnl', 'trade_shares', 'trade_type', 'trade_side'], index=trading_day.index) # MAIN EVENT LOOP trade_count = 0 order_type = '-' order_side = '-' avg_price = 0.0 previous_pos = 0 current_pos = 0 trade_size = 1 unrealized_pnl = 0.0 realized_pnl = 0.0 # track state and values for a current working order live_order = False live_order_price = 0.0 live_order_quantity = 0.0 # other order and market variables # fair value pricing variables midpoint = 0.0 fair_value = 0.0 # define our accumulator for the tick EMA message_type = 0 tick_factor = 0 tick_ema_alpha = 2 / (tick_window + 1) prev_tick = 0 this_tick = 0 # risk factor for part 2 # TODO: implement and evaluate risk_coef = -1.0 prev_risk = 0 this_risk = 0 # signals signal: int = 0 log_message('starting main loop') for index, row in trading_day.iterrows(): # MARKET DATA HANDLING # When it's quote data if pd.isna(row.trade_px): # it's a quote # skip if not NBBO if not ((row.qu_source == 'N') and (row.natbbo_ind == 4)): continue # set our local NBBO variables if (row.bid_px > 0 and row.bid_size > 0): bid_price = row.bid_px # bid_size = row.bid_size * round_lot if (row.ask_px > 0 and row.ask_size > 0): ask_price = row.ask_px # ask_size = row.ask_size * round_lot message_type = 'q' # When it's trade data else: # it's a trade # store the last trade price prev_price = last_price # now get the new data last_price = row.trade_px last_size = row.trade_size message_type = 't' # TICK FACTOR # only update if it's a trade if message_type == 't': # calc the tick this_tick = np.sign(last_price - prev_price) if this_tick == 0: this_tick = prev_tick # now calc the tick if tick_factor == 0: tick_factor = this_tick else: tick_factor = (tick_ema_alpha * this_tick) + (1 - tick_ema_alpha) * tick_factor # store the last tick prev_tick = this_tick # TODO: For Part 2 Incorporate the Risk Factor # RISK FACTOR # use sigmoid function to map any real number to the range(-1,1) if message_type == 't' and risk_adj == 1: # call the risk this_risk = (1/spread_factor)*sigmoid(current_pos * avg_price / risk_denominator) if this_risk == 0: this_risk = prev_risk # now calculate the risk factor if risk_factor == 0: risk_factor = this_risk else: risk_factor = this_risk #store the last risk prev_risk = this_risk # PRICING LOGIC new_midpoint = bid_price + (ask_price - bid_price) / 2 if new_midpoint > 0: midpoint = new_midpoint # FAIR VALUE CALCULATION # check inputs, skip of the midpoint is zero, we've got bogus data (or we're at start of day) if midpoint == 0: continue fair_value = midpoint + (ask_price - bid_price)/spread_factor * ((tick_coef * tick_factor) + (risk_coef * risk_factor)) # collect our data # fair_values[ index ] = fair_value # midpoints[ index ] = midpoint # tick_factors[ index ] = tick_factor # TODO: add collectors for new factors # TRADING LOGIC # update signal signal = np.sign(last_price - prev_price) # LONG POSITION if current_pos > 0: # long position # CHECK for live limit order first if live_order: if (order_side == 'b') and (last_price <= live_order_price): order_type = 'Pas' #passive trade # update P&L unrealized_pnl = current_pos * (last_price - avg_price) # realized_pnl unchanged # update position previous_pos = current_pos current_pos = current_pos + live_order_quantity # update avg(buy) price avg_price = (previous_pos * avg_price + live_order_quantity * live_order_price) / current_pos # now place our aggressive order and record trade information record_trade(trade_df=trades, idx=index, tick=signal, risk=risk_factor, fair_value=fair_value, market_price=last_price, trade_price=live_order_price, avg_price=avg_price, position=current_pos, unrealized_pnl=unrealized_pnl, realized_pnl=realized_pnl, trade_shares=trade_size, trade_type=order_type, trade_side=order_side) # deal with live order live_order_quantity = 0.0 live_order_price = 0.0 live_order = False continue elif (order_side == 's') and (last_price >= live_order_price): order_type = 'Pas' # update P&L unrealized_pnl = calculate_unrealized_pnl(position=current_pos, last_price=last_price, avg_price=avg_price) realized_pnl = calculate_realized_pnl(realized_pnl=realized_pnl, trade_size=trade_size, order_price=live_order_price, avg_price=avg_price) # update position current_pos = current_pos - live_order_quantity # avg(buy) price unchanged # now place our order and record trade information record_trade(trade_df=trades, idx=index, tick=signal, risk=risk_factor, fair_value=fair_value, market_price=last_price, trade_price=live_order_price, avg_price=avg_price, position=current_pos, unrealized_pnl=unrealized_pnl, realized_pnl=realized_pnl, trade_shares=trade_size, trade_type=order_type, trade_side=order_side) # deal with live order live_order = False live_order_price = 0.0 live_order_quantity = 0.0 continue # TODO: determine if we want to buy or sell if signal == 1: # if signal appears and we hold a short position or zero position, buy! order_side = 'b' # if fair price is > ask, buy agg if fair_value >= ask_price: order_type = 'Agg' # trade_price = ask_price ; trade_size = +100 # update P&L unrealized_pnl = calculate_unrealized_pnl(position=current_pos, last_price=last_price, avg_price=avg_price) # realized_pnl unchanged # update position previous_pos = current_pos current_pos = current_pos + trade_size # update avg(buy) price avg_price = (previous_pos * avg_price + trade_size * ask_price) / current_pos # now place our aggressive order and record trade information record_trade(trade_df=trades, idx=index, tick=signal, risk=risk_factor, fair_value=fair_value, market_price=last_price, trade_price=ask_price, avg_price=avg_price, position=current_pos, unrealized_pnl=unrealized_pnl, realized_pnl=realized_pnl, trade_shares=trade_size, trade_type=order_type, trade_side=order_side) # deal with live order live_order_quantity = 0.0 live_order_price = 0.0 live_order = False # if fair price is > bid, buy passive else: # order_type = 'Pas' # position unchanged # avg(buy) price unchanged # update P&L # unrealized_pnl = current_pos * (last_price - avg_price) # realized_pnl unchanged # don't need to record trade information # send limit order live_order_quantity = 1 * trade_size # = +100 live_order_price = bid_price live_order = True elif signal == -1: order_side = 's' # if fair price is < bid, sell agg if fair_value <= bid_price: order_type = 'Agg' # trade_price = bid_price ; trade_size = -100 # update P&L unrealized_pnl = calculate_unrealized_pnl(position=current_pos, last_price=last_price, avg_price=avg_price) realized_pnl = calculate_realized_pnl(realized_pnl=realized_pnl, trade_size=trade_size, order_price=bid_price, avg_price=avg_price) # update position current_pos = current_pos - trade_size # avg(buy) price unchanged # now place our aggressive order and record trade information record_trade(trade_df=trades, idx=index, tick=signal, risk=risk_factor, fair_value=fair_value, market_price=last_price, trade_price=bid_price, avg_price=avg_price, position=current_pos, unrealized_pnl=unrealized_pnl, realized_pnl=realized_pnl, trade_shares=trade_size, trade_type=order_type, trade_side=order_side) # deal with live order live_order_quantity = 0.0 live_order_price = 0.0 live_order = False # if fair price is < ask, sell passive else: # order_type = 'Pas' # position unchanged # avg(buy) price unchanged # update P&L # unrealized_pnl = calculate_unrealized_pnl(position=current_pos, last_price=last_price, avg_price=avg_price) # realized_pnl unchanged # don't need to record trade information # send limit order live_order_quantity = 1 * trade_size # = 100 live_order_price = ask_price live_order = True else: # signal = 0 continue # SHORT POSITION elif current_pos < 0: # short position # CHECK for live limit order first if live_order: if (order_side == 'b') and (last_price <= live_order_price): order_type = 'Pas' # update P&L unrealized_pnl = calculate_unrealized_pnl(position=current_pos, last_price=last_price, avg_price=avg_price) realized_pnl = calculate_realized_pnl(realized_pnl=realized_pnl, trade_size=trade_size, order_price=live_order_price, avg_price=avg_price) # update position current_pos = current_pos + live_order_quantity # avg(sell) price unchanged # now place our aggressive order and record trade information record_trade(trade_df=trades, idx=index, tick=signal, risk=risk_factor, fair_value=fair_value, market_price=last_price, trade_price=live_order_price, avg_price=avg_price, position=current_pos, unrealized_pnl=unrealized_pnl, realized_pnl=realized_pnl, trade_shares=trade_size, trade_type=order_type, trade_side=order_side) # deal with live order live_order_quantity = 0.0 live_order_price = 0.0 live_order = False continue elif (order_side == 's') and (last_price >= live_order_price): order_type = 'Pas' # update P&L unrealized_pnl = calculate_unrealized_pnl(position=current_pos, last_price=last_price, avg_price=avg_price) # realized_pnl unchanged # update position previous_pos = current_pos current_pos = current_pos - live_order_quantity # update avg(buy) price avg_price = (previous_pos * avg_price - live_order_quantity * live_order_price) / current_pos # now place our aggressive order and record trade information record_trade(trade_df=trades, idx=index, tick=signal, risk=risk_factor, fair_value=fair_value, market_price=last_price, trade_price=live_order_price, avg_price=avg_price, position=current_pos, unrealized_pnl=unrealized_pnl, realized_pnl=realized_pnl, trade_shares=trade_size, trade_type=order_type, trade_side=order_side) # deal with live order live_order = False live_order_price = 0.0 live_order_quantity = 0.0 continue # TODO: determine if we want to buy or sell if signal == 1: # if signal appears and we hold a short position or zero position, buy! order_side = 'b' # if fair price is > ask, buy agg if fair_value >= ask_price: order_type = 'Agg' # trade_price = ask_price ; trade_size = +100 # update P&L unrealized_pnl = calculate_unrealized_pnl(position=current_pos, last_price=last_price, avg_price=avg_price) realized_pnl = calculate_realized_pnl(realized_pnl=realized_pnl, trade_size=trade_size, order_price=ask_price, avg_price=avg_price) # update position current_pos = current_pos + trade_size # long shares to close previous short position or to open new position # avg(sell) price unchanged # now place our aggressive order and record trade information record_trade(trade_df=trades, idx=index, tick=signal, risk=risk_factor, fair_value=fair_value, market_price=last_price, trade_price=ask_price, avg_price=avg_price, position=current_pos, unrealized_pnl=unrealized_pnl, realized_pnl=realized_pnl, trade_shares=trade_size, trade_type=order_type, trade_side=order_side) # deal with live order live_order_quantity = 0.0 live_order_price = 0.0 live_order = False # if fair price is > bid, buy passive else: # order_type = 'Pas' # position unchanged # avg(buy) price unchanged # update P&L # unrealized_pnl = calculate_unrealized_pnl(position=current_pos, last_price=last_price, avg_price=avg_price) # realized_pnl unchanged # don't need to record trade information # send limit order live_order_quantity = 1 * trade_size # = +100 live_order_price = bid_price live_order = True elif signal == -1: order_side = 's' # if fair price is < bid, sell agg if fair_value <= bid_price: order_type = 'Agg' # trade_price = bid_price ; trade_size = -100 # update P&L unrealized_pnl = calculate_unrealized_pnl(position=current_pos, last_price=last_price, avg_price=avg_price) # realized_pnl unchanged # update position previous_pos = current_pos current_pos = current_pos - trade_size # update avg(buy) price trade_count += 1 avg_price = (previous_pos * avg_price - trade_size * bid_price) / current_pos # now place our aggressive order and record trade information record_trade(trade_df=trades, idx=index, tick=signal, risk=risk_factor, fair_value=fair_value, market_price=last_price, trade_price=bid_price, avg_price=avg_price, position=current_pos, unrealized_pnl=unrealized_pnl, realized_pnl=realized_pnl, trade_shares=trade_size, trade_type=order_type, trade_side=order_side) # deal with live order live_order_quantity = 0.0 live_order_price = 0.0 live_order = False # if fair price is < ask, sell passive else: # order_type = 'Pas' # position unchanged # avg(buy) price unchanged # update P&L # unrealized_pnl = calculate_unrealized_pnl(position=current_pos, last_price=last_price, avg_price=avg_price) # realized_pnl unchanged # don't need to record trade information # send limit order live_order_quantity = 1 * trade_size # = 100 live_order_price = ask_price live_order = True else: # signal = 0 continue # ZERO POSITION else: # position = 0 # clear the avg_price and trade_count so next transaction can reset avg price of position avg_price = 0 trade_count = 0 # CHECK for live limit order first if live_order: if (order_side == 'b') and (last_price <= live_order_price): order_type = 'Pas' # update P&L unrealized_pnl = calculate_unrealized_pnl(position=current_pos, last_price=last_price, avg_price=avg_price) # realized_pnl unchanged # update position previous_pos = current_pos current_pos = current_pos + live_order_quantity # update avg(buy) price avg_price = (previous_pos * avg_price + live_order_quantity * live_order_price) / current_pos # now place our aggressive order and record trade information record_trade(trade_df=trades, idx=index, tick=signal, risk=risk_factor, fair_value=fair_value, market_price=last_price, trade_price=live_order_price, avg_price=avg_price, position=current_pos, unrealized_pnl=unrealized_pnl, realized_pnl=realized_pnl, trade_shares=trade_size, trade_type=order_type, trade_side=order_side) # deal with live order live_order_quantity = 0.0 live_order_price = 0.0 live_order = False continue elif (order_side == 's') and (last_price >= live_order_price): order_type = 'Pas' # update P&L unrealized_pnl = calculate_unrealized_pnl(position=current_pos, last_price=last_price, avg_price=avg_price) # realized_pnl unchanged # update position previous_pos = current_pos current_pos = current_pos - live_order_quantity # update avg(buy) price avg_price = (previous_pos * avg_price - live_order_quantity * live_order_price) / current_pos # now place our aggressive order and record trade information record_trade(trade_df=trades, idx=index, tick=signal, risk=risk_factor, fair_value=fair_value, market_price=last_price, trade_price=live_order_price, avg_price=avg_price, position=current_pos, unrealized_pnl=unrealized_pnl, realized_pnl=realized_pnl, trade_shares=trade_size, trade_type=order_type, trade_side=order_side) # deal with live order live_order = False live_order_price = 0.0 live_order_quantity = 0.0 continue # TODO: determine if we want to buy or sell if signal == 1: # if signal appears and we hold a short position or zero position, buy! order_side = 'b' # if fair price is > ask, buy agg if fair_value >= ask_price: order_type = 'Agg' # trade_price = ask_price ; trade_size = +100 # update P&L unrealized_pnl = calculate_unrealized_pnl(position=current_pos, last_price=last_price, avg_price=avg_price) # realized_pnl unchanged # update position previous_pos = current_pos current_pos = current_pos + trade_size # update avg(buy) price avg_price = (previous_pos * avg_price + trade_size * ask_price) / current_pos # now place our aggressive order and record trade information record_trade(trade_df=trades, idx=index, tick=signal, risk=risk_factor, fair_value=fair_value, market_price=last_price, trade_price=ask_price, avg_price=avg_price, position=current_pos, unrealized_pnl=unrealized_pnl, realized_pnl=realized_pnl, trade_shares=trade_size, trade_type=order_type, trade_side=order_side) # deal with live order live_order_quantity = 0.0 live_order_price = 0.0 live_order = False # if fair price is > bid, buy passive else: # order_type = 'Pas' # position unchanged # avg(buy) price unchanged # update P&L # unrealized_pnl = current_pos * (last_price - avg_price) # realized_pnl unchanged # don't need to record trade information # send limit order live_order_quantity = 1 * trade_size # = +100 live_order_price = bid_price live_order = True elif signal == -1: order_side = 's' # if fair price is < bid, sell agg if fair_value <= bid_price: order_type = 'Agg' # trade_price = bid_price ; trade_size = -100 # update P&L unrealized_pnl = calculate_unrealized_pnl(position=current_pos, last_price=last_price, avg_price=avg_price) # realized_pnl unchanged # update position previous_pos = current_pos current_pos = current_pos - trade_size # update avg(buy) price avg_price = (previous_pos * avg_price - trade_size * bid_price) / current_pos # now place our aggressive order and record trade information record_trade(trade_df=trades, idx=index, tick=signal, risk=risk_factor, fair_value=fair_value, market_price=last_price, trade_price=bid_price, avg_price=avg_price, position=current_pos, unrealized_pnl=unrealized_pnl, realized_pnl=realized_pnl, trade_shares=trade_size, trade_type=order_type, trade_side=order_side) # deal with live order live_order_quantity = 0.0 live_order_price = 0.0 live_order = False else: # order_type = 'Pas' # position unchanged # avg(buy) price unchanged # update P&L # unrealized_pnl = calculate_unrealized_pnl(position=current_pos, last_price=last_price, avg_price=avg_price) # realized_pnl unchanged # don't need to record trade information # send limit order live_order_quantity = 1 * trade_size # = -100 live_order_price = ask_price live_order = True else: # signal = 0 continue # looping done log_message('end simulation loop') log_message('order analytics') # Now, let's look at some stats trades = trades.dropna() log_message('Algo run complete.') # assemble results and return # TODO: add P&L return trades
def algo_loop(trading_day): log_message('Beginning Tick Strategy run') #log_message( 'TODO: remove this message. Simply a test to see how closely you are reading this code' ) round_lot = 100 avg_spread = (trading_day.ask_px - trading_day.bid_px).mean() half_spread = avg_spread / 2 print("Average stock spread for sample: {:.4f}".format(avg_spread)) # init our price and volume variables [last_price, last_size, bid_price, bid_size, ask_price, ask_size, volume] = np.zeros(7) # init some time series objects for collection of telemetry fair_values = pd.Series(index=trading_day.index) midpoints = pd.Series(index=trading_day.index) tick_factors = pd.Series(index=trading_day.index) risk_factors = pd.Series(index=trading_day.index) # let's set up a container to hold trades. preinitialize with the index trades = pd.DataFrame(columns=[ 'current_tick', 'stock_price', 'current_position', 'current_pnl', 'trade_price', 'trade_shares', 'trade_type', 'trade_side' ], index=trading_day.index) # MAIN EVENT LOOP live_order_index = trading_day.index[0] start_price = 0.0 # price when open the position prev_price = 0 current_pos = 0.0 trade_size = 100 current_pnl = 0.0 total_pnl = 0.0 # track state and values for a current working order live_order = False live_order_price = 0.0 live_order_quantity = 0.0 order_side = '-' # other order and market variables # fair value pricing variables midpoint = 0.0 fair_value = 0.0 # define our accumulator for the tick EMA message_type = 0 tick_coef = 1 tick_window = 20 tick_factor = 0 tick_ema_alpha = 2 / (tick_window + 1) prev_tick = 0 this_tick = 0 # risk factor for part 2 # TODO: implement and evaluate risk_factor = 0.0 risk_coef = 0.0 # signals tick_signal = 0 log_message('starting main loop') for index, row in trading_day.iterrows(): # get the time of this message time_from_open = (index - pd.Timedelta(hours=9, minutes=30)) minutes_from_open = (time_from_open.hour * 60) + time_from_open.minute # MARKET DATA HANDLING # When it's quote data if pd.isna(row.trade_px): # it's a quote # skip if not NBBO if not ((row.qu_source == 'N') and (row.natbbo_ind == 4)): continue # set our local NBBO variables if (row.bid_px > 0 and row.bid_size > 0): bid_price = row.bid_px #bid_size = row.bid_size * round_lot if (row.ask_px > 0 and row.ask_size > 0): ask_price = row.ask_px #ask_size = row.ask_size * round_lot message_type = 'q' #When it's trade data else: # it's a trade # store the last trade price prev_price = last_price # now get the new data last_price = row.trade_px #last_size = row.trade_size message_type = 't' # CHECK OPEN ORDER(S) if we have a live order, # has it been filled by the trade that just happened? if live_order: if (order_side == 'b') and (last_price <= live_order_price): # # even if we only got partially filled, let's assume the entire live order quantity can be filled. fill_size = live_order_quantity # = +100 # update current position current_pnl = current_pos * (live_order_price - start_price) current_pos = current_pos + fill_size # record trading data (previous index) record_trade(trades, live_order_index, prev_tick, last_price, current_pos, current_pnl, live_order_price, fill_size, 'p', order_side) total_pnl += current_pnl # update start price if current_pos > 0: start_price = live_order_price elif current_pos == 0: start_price = 0.0 # deal with live order live_order = False live_order_price = 0.0 live_order_quantity = 0.0 if (order_side == 's') and (last_price >= live_order_price): # even if we only got partially filled, let's assume the entire live order quantity can be filled. fill_size = live_order_quantity # = -100 # update current position current_pnl = current_pos * (live_order_price - start_price) current_pos = current_pos + fill_size # record trading data record_trade(trades, live_order_index, prev_tick, last_price, current_pos, current_pnl, live_order_price, fill_size, 'p', order_side) total_pnl += current_pnl # update start price if current_pos < 0: start_price = live_order_price elif current_pos == 0: start_price = 0.0 # deal with live order live_order = False live_order_price = 0.0 live_order_quantity = 0.0 # TICK FACTOR # only update if it's a trade if message_type == 't': # calc the tick this_tick = np.sign(last_price - prev_price) if this_tick == 0: this_tick = prev_tick # now calc the tick if tick_factor == 0: tick_factor = this_tick else: tick_factor = (tick_ema_alpha * this_tick ) + (1 - tick_ema_alpha) * tick_factor # store the last tick prev_tick = this_tick # RISK FACTOR # TODO: For Part 2 Incorporate the Risk Factor # PRICING LOGIC new_midpoint = bid_price + (ask_price - bid_price) / 2 if new_midpoint > 0: midpoint = new_midpoint # FAIR VALUE CALCULATION # check inputs, skip of the midpoint is zero, we've got bogus data (or we're at start of day) if midpoint == 0: #print( "{} no midpoint. b:{} a:{}".format( index, bid_price, ask_price ) ) continue fair_value = midpoint + half_spread * ( (tick_coef * tick_factor) + (risk_coef * risk_factor)) #tick_coef = 1 # collect our data fair_values[index] = fair_value midpoints[index] = midpoint #tick_factors[ index ] = tick_factor # TODO: add collectors for new factors # risk_factors[ index ] = # TRADING LOGIC # update signal tick_signal = this_tick # TODO: determine if we want to buy or sell if (tick_signal == 1) and ( current_pos <= 0 ): # if signal appears and we hold a short position or zero position, buy! order_side = 'b' # if fair price is > ask, buy agg if fair_value >= ask_price: new_trade_price = ask_price new_trade_size = 1 * trade_size # = +100 # update P&L and position current_pnl = current_pos * (new_trade_price - start_price) current_pos = current_pos + new_trade_size # long shares to close previous short position or to open new position # now place our aggressive order and record trade information record_trade(trades, index, tick_signal, last_price, current_pos, current_pnl, new_trade_price, new_trade_size, 'a', order_side) total_pnl += current_pnl # update start price if current_pos > 0: start_price = new_trade_price elif current_pos == 0: start_price = 0 # deal with live order live_order_quantity = 0.0 live_order_price = 0.0 live_order = False else: # if fair price is > bid, buy passive # update P&L current_pnl = current_pos * (last_price - start_price) record_trade(trades, index, tick_signal, last_price, current_pos, current_pnl, 0, 0) # send limit order live_order_index = index live_order_price = fair_value live_order_quantity = tick_signal * trade_size # = +100 live_order = True elif (tick_signal == -1) and (current_pos >= 0): order_side = 's' # if fair price is < bid, sell agg if fair_value <= bid_price: new_trade_price = bid_price new_trade_size = -1 * trade_size # = -100 # update P&L and position current_pnl = current_pos * (new_trade_price - start_price) current_pos = current_pos + new_trade_size # short shares to close previous long position or to open new position # now place our aggressive order and record trade information record_trade(trades, index, tick_signal, last_price, current_pos, current_pnl, new_trade_price, new_trade_size, 'a', order_side) total_pnl += current_pnl # update start price if current_pos < 0: start_price = new_trade_price elif current_pos == 0: start_price = 0 # deal with live order live_order_quantity = 0.0 live_order_price = 0.0 live_order = False else: # if fair price is < ask, sell passive # update P&L current_pnl = current_pos * (last_price - start_price) record_trade(trades, index, tick_signal, last_price, current_pos, current_pnl, 0, 0) # send limit order live_order_index = index live_order_price = fair_value live_order_quantity = tick_signal * trade_size # = -100 live_order = True else: # no order here. for now just continue current_pnl = current_pos * (last_price - start_price) record_trade(trades, index, tick_signal, last_price, current_pos, current_pnl, 0, 0) continue prev_index = index # looping done log_message('end simulation loop') log_message('order analytics') # Now, let's look at some stats trades = trades.dropna() log_message('Algo run complete.') # assemble results and return # TODO: add P&L return {'trades': trades, 'total_pnl': total_pnl}
def algo_loop(trading_day, factors_included=[1, 3, 4, 5], coef_included=[2, 8e-4, 10, 10], limit_orders_included=True, quantity_coef_=4000000, display_factors=False): factors_included = np.array(factors_included) if len(factors_included) != len(coef_included): print( 'Factors included need to be the same length as coefficients included' ) return 0 log_message('Beginning Tick Strategy run') # Average spread is calculated with the NBBO prices avg_spread = ( trading_day[((trading_day.qu_source == 'N') & (trading_day.natbbo_ind == 4)) | ((trading_day.qu_source == 'C') & (trading_day.natbbo_ind == 'G'))].ask_px - trading_day[((trading_day.qu_source == 'N') & (trading_day.natbbo_ind == 4)) | ((trading_day.qu_source == 'C') & (trading_day.natbbo_ind == 'G'))].bid_px).mean() # Calculating half the spread (used in factors) half_spread = avg_spread / 2 print("Average stock spread for sample: {:.4f}".format(avg_spread)) # init our price and volume variables [last_price, last_size, bid_price, bid_size, ask_price, ask_size, volume] = np.zeros(7) # init our counters [trade_count, quote_count, cumulative_volume] = [0, 0, 0] # init some time series objects for collection of telemetry fair_values = pd.Series(index=trading_day.index) midpoints = pd.Series(index=trading_day.index) bid_values = pd.Series(index=trading_day.index) ask_values = pd.Series(index=trading_day.index) tick_factors = pd.Series(index=trading_day.index) risk_factors = pd.Series( index=trading_day.index) #-Added risk factor series - Matt c1_factors = pd.Series(index=trading_day.index) c2_factors = pd.Series(index=trading_day.index) c3_factors = pd.Series(index=trading_day.index) c4_factors = pd.Series(index=trading_day.index) c5_factors = pd.Series(index=trading_day.index) ma_factors = pd.Series(index=trading_day.index) indicator_list = np.zeros(5) # let's set up a container to hold trades. preinitialize with the index trades = pd.DataFrame( columns=['price', 'shares', 'bar', 'trade_type', 'side'], index=trading_day.index) ma_df = pd.DataFrame(columns=['MA_LT', 'MA_ST', 'MA_LT_m', 'MA_ST_m'], index=trading_day.index) # Try to make this more general for many days trades_candle = pd.DataFrame( columns=['shares', 'side', 'filled'], index=[ datetime.strptime( str(trading_day.index[0])[:10] + ' 09:30:00', '%Y-%m-%d %H:%M:%S') + timedelta(minutes=x) for x in range(391) ]) candlesticks_df = pd.DataFrame( columns=['open', 'close', 'high', 'low'], index=[ datetime.strptime( str(trading_day.index[0])[:10] + ' 09:30:00', '%Y-%m-%d %H:%M:%S') + timedelta(minutes=x) for x in range(391) ]) candlestick_indicators = pd.DataFrame( columns=[4, 5, 6, 7, 8] + ['trades'], index=[ datetime.strptime( str(trading_day.index[0])[:10] + ' 09:30:00', '%Y-%m-%d %H:%M:%S') + timedelta(minutes=x) for x in range(391) ]) # MAIN EVENT LOOP current_bar = 0 net_position = 0 # track state and values for a current working order live_order = False live_order_price = 0.0 live_order_quantity = 0.0 order_side = '-' bid_price = 0 ask_price = 0 # other order and market variables total_quantity_filled = 0 vwap_numerator = 0.0 total_trade_count = 0 total_agg_count = 0 total_pass_count = 0 # fair value pricing variables midpoint = 0.0 fair_value = 0.0 message_type = 0 # Moving average ma_factor = 0.0 if 1 in factors_included: ma_coef = coef_included[np.where(factors_included == 1)[0][0]] #2#0.1 temp = trading_day[trading_day.trade_px.isna() == False].shape[0] ma_interval = math.floor(temp * 5 / 390) ma_interval_long = math.floor(temp * 20 / 390) ma_vector = np.zeros(ma_interval) ma_vector_long = np.zeros(ma_interval_long) ma_vector_plot = np.zeros(ma_interval) ma_vector_long_plot = np.zeros(ma_interval_long) ma_i = 0 ma_i_long = 0 ma_bool = True else: ma_coef = 0 # define our accumulator for the tick EMA tick_factor = 0.0 if 2 in factors_included: tick_coef = coef_included[np.where(factors_included == 2)[0][0]] #1.3 tick_window = 30 tick_ema_alpha = 2 / (tick_window + 1) prev_tick = 0 prev_price = 0 else: tick_coef = 0.0 # risk factor for part 2 risk_factor = 0.0 if 3 in factors_included: risk_coef = coef_included[np.where(factors_included == 3)[0][0]] #8e-4 else: risk_coef = 0 # Candlestick factor 1 c1_factor = 0.0 if 4 in factors_included: c1_coef = coef_included[np.where(factors_included == 4)[0][0]] #10 #1 c1_shift = 5 else: c1_coef = 0 c1_shift = 0 # Candlestick factor 2 c2_factor = 0.0 if 5 in factors_included: c2_coef = coef_included[np.where(factors_included == 5)[0][0]] #10 c2_shift = 10 else: c2_coef = 0 c2_shift = 0 # Candlestick factor 3 c3_factor = 0.0 if 6 in factors_included: c3_coef = coef_included[np.where(factors_included == 6)[0][0]] # 10 c3_shift = 60 else: c3_coef = 0.0 c3_shift = 0.0 # Candlestick factor 4 c4_factor = 0.0 if 7 in factors_included: c4_coef = coef_included[np.where(factors_included == 7)[0][0]] c4_shift = 30 else: c4_coef = 0.0 c4_shift = 0 # Candlestick factor 5 c5_factor = 0.0 if 8 in factors_included: c5_coef = coef_included[np.where(factors_included == 8)[0][0]] c5_shift = 30 else: c5_coef = 0.0 c5_shift = 0 current_minute = 0 last_minute = 0 last_high = 0 last_low = 0 last_open = 0 last_close = 0 current_high = 0 current_low = 0 current_open = 0 current_close = 0 first_minute = True # factors calculating the quantity to buy and sell quantity_coef = quantity_coef_ #4000000 # Floor on the minimum amount we need to place a limit order min_lim_order = 50 # Variable that takes care of the fact that a passive order and aggressive order can be completed at the same time. # Instead of overwriting the trade in trades_df, we only allow one trade each iteration live_order_executed = False candlestick_calculation = False log_message('starting main loop') for index, row in trading_day.iterrows(): last_minute = current_minute current_minute = index.minute # Change the if sentence if we have more than 1 day of data # IT WORKS FOR MANY DAYS if current_minute > last_minute or (current_minute == 0 and last_minute == 59) or first_minute: if first_minute: first_minute = not first_minute else: candlestick_calculation = True last_high = current_high last_low = current_low last_open = current_open last_close = current_close if current_minute == 0: last_hour = index.hour - 1 else: last_hour = index.hour current_minute_data = trading_day[ (trading_day.index.hour == last_hour) & (trading_day.index.minute == last_minute) & (trading_day.trade_px.isna() == False)] temp = current_minute_data['trade_px'] if len(temp) == 0: current_high = last_close current_low = last_close current_open = last_close current_close = last_close else: current_high = temp.max() current_low = temp.min() current_open = temp[0] current_close = temp[-1] # get the time of this message time_from_open = (index - pd.Timedelta(hours=9, minutes=30)) minutes_from_open = (time_from_open.hour * 60) + time_from_open.minute ## Have to fix this because if a trade comes in, it has to fill the limit order as well ## but not just perform the candlestick trade and nothing more if candlestick_calculation: new_index = str(index)[:10] + ' ' + str(index.hour).zfill(2) + ':' + \ str(index.minute).zfill(2) + ':00' record_candlesticks(candlesticks_df, new_index, current_open, current_close, current_high, current_low) if trades_candle['shares'][(trades_candle.index <= new_index) & ( trades_candle['filled'] == 0)].sum() != 0: live_order_executed = True if trades_candle['shares'][ (trades_candle.index <= new_index) & (trades_candle['filled'] == 0)].sum() > 0: print('Closing Position') temp_side = 's' record_trade( trades, index, bid_price, trades_candle['shares'] [(trades_candle.index <= new_index) & (trades_candle['filled'] == 0)].sum(), current_bar, 'a', temp_side) net_position = net_position - trades_candle['shares'][ (trades_candle.index <= new_index) & (trades_candle['filled'] == 0)].sum() total_quantity_filled += trades_candle['shares'][ (trades_candle.index <= new_index) & (trades_candle['filled'] == 0)].sum() total_agg_count += 1 trades_candle['filled'][(trades_candle.index <= new_index) & (trades_candle['filled'] == 0)] = 1 else: print('Closing Position') temp_side = 'b' record_trade( trades, index, ask_price, -trades_candle['shares'][ (trades_candle.index <= new_index) & (trades_candle['filled'] == 0)].sum(), current_bar, 'a', temp_side) net_position = net_position - trades_candle['shares'][ (trades_candle.index <= new_index) & (trades_candle['filled'] == 0)].sum() total_quantity_filled -= trades_candle['shares'][ (trades_candle.index <= new_index) & (trades_candle['filled'] == 0)].sum() total_agg_count += 1 trades_candle['filled'][(trades_candle.index <= new_index) & (trades_candle['filled'] == 0)] = 1 # MARKET DATA HANDLING if pd.isna(row.trade_px): # it's a quote # skip if not NBBO if not (((row.qu_source == 'N') and (row.natbbo_ind == 4)) or ((row.qu_source == 'C') and (row.natbbo_ind == 'G'))): candlestick_calculation = False bid_values[index] = bid_price ask_values[index] = ask_price continue # set our local NBBO variables if (row.bid_px > 0 and row.bid_size > 0): bid_price = row.bid_px bid_size = row.bid_size if (row.ask_px > 0 and row.ask_size > 0): ask_price = row.ask_px ask_size = row.ask_size quote_count += 1 message_type = 'q' else: # it's a trade # store the last trade price prev_price = last_price # now get the new data last_price = row.trade_px last_size = row.trade_size trade_count += 1 cumulative_volume += row.trade_size vwap_numerator += last_size * last_price message_type = 't' # CHECK OPEN ORDER(S) if we have a live order, # has it been filled by the trade that just happened? if live_order and not live_order_executed and limit_orders_included: if (order_side == 'b') and (last_price <= live_order_price): #Our side hasn't updated because once it does, the live order is cancelled print('Limit buy @ %.3f' % (live_order_price)) fill_size = min(live_order_quantity, last_size) record_trade(trades, index, live_order_price, fill_size, current_bar, 'p', order_side) net_position = net_position + fill_size total_quantity_filled += fill_size total_pass_count += 1 # We chose to not cancel the limit order once a part of it was filled. # We update the quantity if fill_size >= live_order_quantity: live_order = False live_order_price = 0.0 live_order_quantity = 0.0 else: live_order_quantity -= fill_size live_order_executed = True if (order_side == 's') and (last_price >= live_order_price): print('Limit sell @ %.3f' % (live_order_price)) fill_size = min(live_order_quantity, last_size) record_trade(trades, index, live_order_price, fill_size, current_bar, 'p', order_side) net_position = net_position - fill_size total_quantity_filled += fill_size total_pass_count += 1 # We chose to not cancel the limit order once a part of it was filled. # We update the quantity if fill_size >= live_order_quantity: live_order = False live_order_price = 0.0 live_order_quantity = 0.0 else: live_order_quantity -= fill_size live_order_executed = True # MOVING AVERAGE # calc moving average factor if 1 in factors_included: if message_type == 't' and midpoint > 0: #ma_spread = last_spread/2 #if order_side == 'b': ma_vector[ma_i] = last_price #- midpoint#ask_price - ma_spread ma_vector_long[ma_i_long] = last_price #- midpoint ma_vector_plot[ma_i] = last_price ma_vector_long_plot[ma_i_long] = last_price #else: # ma_vector[ma_i] = bid_price + ma_spread if ma_i == ma_interval - 1: ma_i = 0 else: ma_i += 1 if ma_i_long == ma_interval_long - 1: ma_i_long = 0 else: ma_i_long += 1 #i = (i+1)*((ma_interval - 1) != i) if ma_vector[-1] != 0 and ma_vector_long[-1] != 0: st_mean = np.mean(ma_vector) lt_mean = np.mean(ma_vector_long) ma_df['MA_ST'][index] = np.mean(ma_vector_plot) ma_df['MA_LT'][index] = np.mean(ma_vector_long_plot) ma_df['MA_ST_m'][index] = st_mean ma_df['MA_LT_m'][index] = lt_mean if st_mean > lt_mean and not ma_bool: ma_factor = 1 ma_bool = not ma_bool elif st_mean < lt_mean and ma_bool: ma_factor = -1 ma_bool = not ma_bool else: ma_factor = 0 #ma_factor = st_mean - lt_mean else: ma_factor = 0 # TICK FACTOR # only update if it's a trade if 2 in factors_included: if message_type == 't': # calc the tick this_tick = np.sign(last_price - prev_price) if this_tick == 0: this_tick = prev_tick # now calc the tick if tick_factor == 0: tick_factor = this_tick else: tick_factor = (tick_ema_alpha * this_tick ) + (1 - tick_ema_alpha) * tick_factor # store the last tick prev_tick = this_tick # RISK FACTOR # calc the risk factor if 3 in factors_included: if net_position < 0: #Need to buy back shares # Risk factor is dependent on how short we are risk_factor = -net_position elif net_position > 0: # Risk factor is dependent on how long we are risk_factor = -net_position else: # if the net position is 0, we cancel the risk factor risk_factor = 0 # CANDLESTICK 1 FACTOR # DARKCLOUD COVER if 4 in factors_included: c1_factor = 0 if candlestick_calculation: if ((last_close > last_open) and (((last_close + last_open) / 2) > current_close) and (current_open > current_close) and (current_open > last_close) and (current_close > last_open) and ((current_open - current_close) / (.001 + (current_high - current_low)) > 0.6)): c1_factor = 1 #(abs(last_close - current_close) + abs(current_close - current_open))/2 indicator_list[0] = c1_factor # CANDLESTICK 2 FACTOR # DOJI if 5 in factors_included: c2_factor = 0 if candlestick_calculation: if abs(current_close - current_open) / (current_high - current_low) < 0.1 and \ (current_high - max(current_close, current_open)) > (3 * abs(current_close - current_open)) and \ (min(current_close, current_open) - current_low) > (3 * abs(current_close - current_open)): c2_factor = 1 #(abs(last_close - current_close) + abs(current_close - current_open))/2 indicator_list[1] = c2_factor # CANDLESTICK 3 FACTOR # BEARISH ENGULFING if 6 in factors_included: c3_factor = 0 if candlestick_calculation: if (current_open >= last_close > last_open and current_open > current_close and last_open >= current_close and current_open - current_close > last_close - last_open): c3_factor = 1 indicator_list[2] = c3_factor # CANDLESTICK 4 FACTOR # HAMMER if 7 in factors_included: c4_factor = 0 if candlestick_calculation: if (((current_high - current_low) > 3 * (current_open - current_close)) and ((current_close - current_low) / (.001 + current_high - current_low) > 0.6) and ((current_open - current_low) / (.001 + current_high - current_low) > 0.6)): c4_factor = -1 indicator_list[3] = c4_factor # CANDLESTICK 5 FACTOR # INVERTED HAMMER if 8 in factors_included: c5_factor = 0 if candlestick_calculation: if (((current_high - current_low) > 3 * (current_open - current_close)) and ((current_high - current_close) / (.001 + current_high - current_low) > 0.6) and ((current_high - current_open) / (.001 + current_high - current_low) > 0.6)): c5_factor = -1 indicator_list[4] = c5_factor # PRICING LOGIC new_midpoint = bid_price + (ask_price - bid_price) / 2 if new_midpoint > 0: midpoint = new_midpoint # FAIR VALUE CALCULATION # check inputs, skip of the midpoint is zero, we've got bogus data (or we're at start of day) if midpoint == 0: print("{} no midpoint. b:{} a:{}".format(index, bid_price, ask_price)) continue fair_value = midpoint + half_spread * ((risk_coef * risk_factor) + (ma_coef * ma_factor) + (c1_coef * c1_factor) + (c2_coef * c2_factor) + (c3_coef * c3_factor) + (c4_coef * c4_factor) + (c5_coef * c5_factor)) # This prints out all important information. Good to see how the factors are functioning if display_factors: print('fair bid ask mid tick risk') print('%.3f %.3f %.3f %.3f %.3f %.3f' % ((fair_value), (bid_price), (ask_price), (midpoint), (half_spread * (tick_coef * tick_factor)), (half_spread * (risk_coef * risk_factor)))) # collect our data fair_values[index] = fair_value midpoints[index] = midpoint bid_values[index] = bid_price ask_values[index] = ask_price tick_factors[index] = half_spread * ((tick_coef * tick_factor)) risk_factors[index] = half_spread * (risk_coef * risk_factor ) #Added risk_factor to series c1_factors[index] = c1_factor * c1_coef * half_spread c2_factors[index] = c2_factor * c2_coef * half_spread c3_factors[index] = c3_factor * c3_coef * half_spread c4_factors[index] = c4_factor * c4_coef * half_spread c5_factors[index] = c5_factor * c5_coef * half_spread ma_factors[index] = half_spread * ma_coef * ma_factor if candlestick_calculation: record_candlestick_indicators(candlestick_indicators, new_index, np.abs(indicator_list), trade=0) # TRADE DECISION # We want to sell if the fair value is below the midpoint and buy if the fair value is above if fair_value < midpoint: order_side = 's' elif fair_value > midpoint: order_side = 'b' else: order_side = '-' # TRADING LOGIC # check where our FV is versus the BBO and constrain if order_side == 'b': # Don't trade if a limit order was executed in the same iteration if (fair_value >= ask_price) and (live_order_executed == False): new_trade_price = ask_price # now place our aggressive order: assume you can execute the full size across spread new_order_quantity = calc_order_quantity( net_position, midpoint, fair_value, order_side, quantity_coef) new_order_quantity = np.minimum(new_order_quantity, 2000) # If the new order quantity is above 0, we do a trade if new_order_quantity != 0: total_agg_count += 1 hour_index = index.hour if np.any( np.array([ c1_factor, c2_factor, c3_factor, c4_factor, c5_factor ]) > 0): # sum([abs(x) for x in [c1_factor,c2_factor,c3_factor,c4_factor,c5_factor]]) != 0: if c1_factor > 0: time_shift = c1_shift elif c2_factor > 0: time_shift = c2_shift elif c3_factor > 0: time_shift = c3_shift elif c4_factor > 0: time_shift = c4_shift else: time_shift = c5_shift if index.minute + time_shift > 59: #Only works for small time shifts hour_index = index.hour + 1 minute_index = index.minute + time_shift - 60 else: hour_index = index.hour minute_index = index.minute + time_shift if hour_index <= 15: record_candlestick_indicators( candlestick_indicators, new_index, indicator_list, trade=1) new_index = str(trading_day.index[0])[:10] + \ ' ' + str(hour_index).zfill(2) + \ ':' + str(minute_index).zfill(2) + ':00' record_candle_trades(trades_candle, new_index, new_order_quantity, order_side) if hour_index <= 15: record_trade(trades, index, new_trade_price, new_order_quantity, current_bar, 'a', order_side) net_position = net_position + new_order_quantity # Good to visualize the factor values by printing out during a trade if display_factors: print('Fair value: ', fair_value) print('Tick: ', half_spread * ((tick_coef * tick_factor))) print('Risk: ', half_spread * (risk_coef * risk_factor)) # update quantity remaining total_quantity_filled += new_order_quantity live_order_quantity = 0.0 live_order_price = 0.0 live_order = False elif limit_orders_included: # we're not yet willing to cross the spread, stay passive order_quantity = calc_order_quantity(net_position, midpoint, fair_value, order_side, quantity_coef) # Cap our orders to 2000 shares. order_quantity = np.minimum(order_quantity, 2000) # Don't send a limit order if it's too small (to minimize transaction costs) if order_quantity >= min_lim_order: live_order_price = bid_price live_order_quantity = order_quantity live_order = True elif order_side == 's': # Don't trade if a limit order was executed in the same iteration if (fair_value <= bid_price) and (live_order_executed == False): new_trade_price = bid_price # now place our aggressive order: assume you can execute the full size across spread new_order_quantity = calc_order_quantity( net_position, midpoint, fair_value, order_side, quantity_coef) new_order_quantity = np.minimum(new_order_quantity, 2000) if new_order_quantity != 0: total_agg_count += 1 hour_index = index.hour hour_index = index.hour if np.any( np.array([ c1_factor, c2_factor, c3_factor, c4_factor, c5_factor ]) < 0): if c1_factor < 0: time_shift = c1_shift elif c2_factor < 0: time_shift = c2_shift elif c3_factor < 0: time_shift = c3_shift elif c4_factor < 0: time_shift = c4_shift else: time_shift = c5_shift if index.minute + time_shift > 59: #Only works for small time shifts hour_index = index.hour + 1 minute_index = index.minute + time_shift - 60 else: hour_index = index.hour minute_index = index.minute + time_shift if hour_index <= 15: record_candlestick_indicators( candlestick_indicators, new_index, np.abs(indicator_list), trade=-1) new_index = str(trading_day.index[0])[:10] + \ ' ' + str(hour_index).zfill(2) + \ ':' + str(minute_index).zfill(2) + ':00' record_candle_trades(trades_candle, new_index, -new_order_quantity, order_side) if hour_index <= 15: record_trade(trades, index, new_trade_price, new_order_quantity, current_bar, 'a', order_side) net_position = net_position - new_order_quantity # update quantity remaining total_quantity_filled += new_order_quantity # Print factors for convenience if display_factors: print('Fair value: ', fair_value) print('Tick: ', half_spread * ((tick_coef * tick_factor))) print('Risk: ', half_spread * (risk_coef * risk_factor)) live_order_quantity = 0.0 live_order_price = 0.0 live_order = False elif limit_orders_included: # not yet willing to cross spread order_quantity = calc_order_quantity(net_position, midpoint, fair_value, order_side, quantity_coef) # Cap or order quantity to 2000 shares each time order_quantity = np.minimum(order_quantity, 2000) # Don't send a limit order if it's too small (to minimize transaction costs) if order_quantity >= min_lim_order: live_order_price = ask_price live_order_quantity = order_quantity live_order = True else: pass # no order here. for now just continue live_order_executed = False candlestick_calculation = False indicator_list = np.zeros(5) # looping done log_message('end simulation loop') log_message('order analytics') # Now, let's look at some stats trades = trades.dropna() day_vwap = vwap_numerator / cumulative_volume # prep our text output avg_price = 0 if trades['shares'].sum() != 0: avg_price = (trades['price'] * trades['shares']).sum() / trades['shares'].sum() log_message('Algo run complete.') #print(midpoints.loc['2019-10-10 15:57:00.020201161']) [ total_pnl, intra_pnl_df, worst_intra, best_intra, max_long, max_short, min_pnl, max_pnl, min_shares_long, max_shares_long, min_shares_short, max_shares_short ] = trade_statistics(trades, net_position, bid_price, ask_price, midpoints, ask_values, bid_values) statistics = [ total_pnl, intra_pnl_df, worst_intra, best_intra, max_long, max_short, min_pnl, max_pnl, min_shares_long, max_shares_long, min_shares_short, max_shares_short ] # assemble results and return return { 'midpoints': midpoints, 'fair_values': fair_values, 'tick_factors': tick_factors, 'risk_factors': risk_factors, 'c1_factors': c1_factors, 'c2_factors': c2_factors, 'c3_factors': c3_factors, 'c4_factors': c4_factors, 'c5_factors': c5_factors, 'ma_factors': ma_factors, 'trades': trades, 'quote_count': quote_count, 'day_vwap': day_vwap, 'avg_price': avg_price, 'net_position': net_position, 'last_bid': bid_price, 'last_ask': ask_price, 'midpoints': midpoints, 'ask_values': ask_values, 'bid_values': bid_values, 'statistics': statistics, 'candlesticks': candlesticks_df, 'candlestick_indicators': candlestick_indicators, 'ma_df': ma_df }
def algo_loop(trading_day, order_side, original_order_quantity, vwap_coefficients, max_behind, schedule_coef, tick_window, tick_coef, limit_price=None, limit_quantity=np.inf): log_message('Beginning VWAP run: {:s} {:d} shares'.format( order_side, original_order_quantity)) round_lot = 100 avg_spread = (trading_day.ask_px - trading_day.bid_px).mean() half_spread = avg_spread / 2 print("Average stock spread for sample: {:.4f}".format(avg_spread)) # if limit price is not determined, convert it to numerical type if limit_price == None: if order_side == 'b': limit_price = np.inf elif order_side == 's': limit_price = 0 else: print(f'order_side not understood in tick {trading_day.index}') return None # caclulate last_minutes_from_open in a specific trading day last_time_from_open = (trading_day.index[-1] - pd.Timedelta(hours=9, minutes=30)) last_minute_from_open = (last_time_from_open.hour * 60) + last_time_from_open.minute # generate target schedule - use bins 1 - 390 giving an automatic 1 minute "look ahead" # note that targets have been converted to shares from percent order_targets = vwap_target(np.arange(0, 390, dtype='int64'), vwap_coefficients) * original_order_quantity # order_targets = np.round(order_targets) # let's force the last bin to complete order_targets[-1] = original_order_quantity # init our price and volume variables [last_price, last_size, bid_price, bid_size, ask_price, ask_size, volume] = np.zeros(7) # init our counters [trade_count, quote_count, cumulative_volume] = [0, 0, 0] # init some time series objects for collection of telemetry fair_values = pd.Series(index=trading_day.index) midpoints = pd.Series(index=trading_day.index) schedule_factors = pd.Series(index=trading_day.index) tick_factors = pd.Series(index=trading_day.index) # let's set up a container to hold trades. preinitialize with the index trades = pd.DataFrame(columns=['price', 'shares', 'bar', 'trade_type'], index=trading_day.index) #max_behind = 2000 # MAIN EVENT LOOP current_bar = 0 current_target_shares = 0 quantity_behind = 0 # track state and values for a current working order live_order = False live_order_price = 0.0 live_order_quantity = 0.0 # other order and market variables total_quantity_filled = 0 quantity_remaining = original_order_quantity - total_quantity_filled vwap_numerator = 0.0 total_trade_count = 0 total_agg_count = 0 total_pass_count = 0 # fair value pricing variables midpoint = 0.0 fair_value = 0.0 schedule_factor = 0.0 if (order_side == 's') & (schedule_coef >= 0): schedule_coef = -schedule_coef # change the schedule factor sign for sell orders # define our accumulator for the tick EMA message_type = 0 #tick_coef = 1. #tick_window = 20 tick_factor = 0 tick_ema_alpha = 2 / (tick_window + 1) prev_tick = 0 prev_price = 0 log_message('starting main loop') for index, row in trading_day.iterrows(): # get the time of this message time_from_open = (index - pd.Timedelta(hours=9, minutes=30)) minutes_from_open = (time_from_open.hour * 60) + time_from_open.minute # MARKET DATA HANDLING if pd.isna(row.trade_px): # it's a quote # skip if not NBBO #if not ( ( row.qu_source == 'N' ) and ( row.natbbo_ind == 4 ) ): # continue # set our local NBBO variables if (row.bid_px > 0 and row.bid_size > 0): bid_price = row.bid_px bid_size = row.bid_size * round_lot if (row.ask_px > 0 and row.ask_size > 0): ask_price = row.ask_px ask_size = row.ask_size * round_lot quote_count += 1 message_type = 'q' else: # it's a trade # store the last trade price prev_price = last_price # now get the new data last_price = row.trade_px last_size = row.trade_size trade_count += 1 cumulative_volume += row.trade_size vwap_numerator += last_size * last_price message_type = 't' # CHECK OPEN ORDER(S) if we have a live order, # has it been filled by the trade that just happened? if live_order: if (order_side == 'b') and (last_price <= live_order_price): fill_size = min(live_order_quantity, last_size) total_quantity_filled += fill_size total_pass_count += 1 # even if we only got partially filled, let's assume we're cancelling the entire quantity. # If we're still behind we'll replace later in the loop quantity_behind = current_target_shares - total_quantity_filled record_trade(trades, index, live_order_price, fill_size, current_bar, 'p') live_order = False live_order_price = 0.0 live_order_quantity = 0.0 if (order_side == 's') and (last_price >= live_order_price): fill_size = min(live_order_quantity, last_size) total_quantity_filled += fill_size # print("{} passive. quantity_behind:{} new_order_quantity:{}".format(index, quantity_behind, # fill_size)) total_pass_count += 1 # even if we only got partially filled, let's assume we're cancelling the entire quantity. # If we're still behind we'll replace later in the loop quantity_behind = current_target_shares - total_quantity_filled record_trade(trades, index, live_order_price, fill_size, current_bar, 'p') live_order = False live_order_price = 0.0 live_order_quantity = 0.0 # SCHEDULE FACTOR if minutes_from_open > current_bar: # we're in a new bar do new bar things here current_bar = minutes_from_open current_target_shares = order_targets[current_bar] quantity_behind = current_target_shares - total_quantity_filled # uncomment for debug # print("{} bar: {:d} target:{:.2f} behind:{:.2f}".format(index, current_bar, current_target_shares, # quantity_behind)) # TICK FACTOR # only update if it's a trade if message_type == 't': # calc the tick this_tick = np.sign(last_price - prev_price) if this_tick == 0: this_tick = prev_tick # now calc the tick if tick_factor == 0: tick_factor = this_tick else: tick_factor = (tick_ema_alpha * this_tick) + (1 - tick_ema_alpha) * tick_factor # store the last tick prev_tick = this_tick # PRICING LOGIC new_midpoint = bid_price + (ask_price - bid_price) / 2 if new_midpoint > 0: midpoint = new_midpoint schedule_factor = calc_schedule_factor(max_behind, current_target_shares, total_quantity_filled, original_order_quantity) # FAIR VALUE CALCULATION # check inputs, skip of the midpoint is zero, we've got bogus data (or we're at start of day) if midpoint == 0: # print("{} no midpoint. b:{} a:{}".format(index, bid_price, ask_price)) continue fair_value = midpoint + (schedule_coef * schedule_factor * half_spread ) + (tick_coef * tick_factor * half_spread) # collect our data fair_values[index] = fair_value midpoints[index] = midpoint schedule_factors[index] = schedule_factor tick_factors[index] = tick_factor # TRADING LOGIC # check where our FV is versus the BBO and constrain # check if the current tick is passively traded if order_side == 'b': if (fair_value >= ask_price and quantity_behind > round_lot) or ( minutes_from_open == last_minute_from_open and quantity_behind > 0): # if passive trading happened in this tick, # or if ask price is larger than limit price, do not execute aggresive traiding if trades.loc[ index].trade_type == 'p' or ask_price > limit_price: continue total_agg_count += 1 new_trade_price = ask_price # now place our aggressive order: assume you can execute the full size across spread if minutes_from_open == last_minute_from_open: new_order_quantity = min(quantity_behind, limit_quanitity) else: new_order_quantity = min( calc_order_quantity(quantity_behind, round_lot), limit_quantity) #update total quantity filled total_quantity_filled += new_order_quantity # update quantity_behind quantity_behind = current_target_shares - total_quantity_filled record_trade(trades, index, new_trade_price, new_order_quantity, current_bar, 'a') live_order_quantity = 0.0 live_order_price = 0.0 live_order = False else: # we're not yet willing to cross the spread, stay passive if quantity_behind > round_lot and bid_price < limit_price: live_order_price = bid_price live_order_quantity = calc_order_quantity( quantity_behind, round_lot) # live_order_quantity = quantity_behind live_order = True elif order_side == 's': if (fair_value <= bid_price and quantity_behind > round_lot) or ( minutes_from_open == last_minute_from_open and quantity_behind > 0): # if passive trading happened in this tick, do not execute aggresive traiding if trades.loc[ index].trade_type == 'p' or bid_price < limit_price: continue total_agg_count += 1 new_trade_price = bid_price # now place our aggressive order: assume you can execute the full size across spread if minutes_from_open == last_minute_from_open: new_order_quantity = min(quantity_behind, limit_quantity) else: new_order_quantity = min( calc_order_quantity(quantity_behind, round_lot), limit_quantity) # new_order_quantity = quantity_behind # update total quantity filled total_quantity_filled += new_order_quantity # update quantity_behind quantity_behind = current_target_shares - total_quantity_filled record_trade(trades, index, new_trade_price, new_order_quantity, current_bar, 'a') live_order_quantity = 0.0 live_order_price = 0.0 live_order = False else: # not yet willing to cross spread if quantity_behind > round_lot and ask_price > limit_price: live_order_price = ask_price live_order_quantity = calc_order_quantity( quantity_behind, round_lot) live_order = True else: # we shouldn't have got here, something is wrong with our order type print('Got an unexpected order_side value: ' + str(order_side)) # looping done log_message('end simulation loop') log_message('order analytics') # Now, let's look at some stats trades = trades.dropna() day_vwap = vwap_numerator / cumulative_volume # prep our text output avg_price = (trades['price'] * trades['shares']).sum() / trades['shares'].sum() log_message('VWAP run complete.') # assemble results and return return { 'midpoints': midpoints, 'fair_values': fair_values, 'schedule_factors': schedule_factors, 'tick_factors': tick_factors, 'trades': trades, 'quote_count': quote_count, 'day_vwap': day_vwap, 'avg_price': avg_price }
def algo_loop(total_data, fx_list, period_list, leverage=2.0, jpy=0): log_message('Beginning Carry-Trade Strategy run') # equity initialization equity = 10000 # position info initialization current_pos = 0 rate_open = 0 # pnl initialization unreal_pnl = 0 real_pnl = 0 temp_pnl = 0 # perform analysis initialization max_return = 0 max_drawdown = 0 # portfolio info initialziation trade_fx = '-' trade_period = 0 trade_period_name = '-' # set up the trade book trades = pd.DataFrame(columns=[ 'Signal', 'FX_name', 'Period', 'Foreign_IR', 'Domestic_IR', 'FX_Rate', 'Equity', 'Asset Pos', 'Unreal_Return', 'Real_Return', 'Drawdown' ], index=total_data.index) for index, row in total_data.iterrows(): if equity < 0: # We've gone insolvent break if current_pos == 0: # find max signal [max_signal, max_period, max_fx] = find_max_signal(row_row=row, period_list=period_list, fx_list=fx_list) max_period_name = cal_period_name(trading_day=max_period) if max_signal > 0: # record trading fx name and period trade_fx = max_fx # e.g. AUD trade_period = max_period # e.g. 60 trade_period_name = max_period_name # e.g. 2M # record trading day start_day = index end_day = holiday_adjust(start_day, dt.timedelta(days=trade_period)) # Interest rates idx [ fx_libor_idx, dstc_libor_idx, spot_fx_rate_idx, forward_fx_rate_idx, ask_fx_rate_idx, bid_fx_rate_idx ] = cal_rates_name(fx_name=trade_fx, period_name=trade_period_name) # Interest rates trade_r_f = row[fx_libor_idx] / 100 trade_r_d = row[dstc_libor_idx] / 100 if trade_fx == 'JPY': trade_fx_rate = 1 trade_ask_fx_rate = 1 trade_bid_fx_rate = 1 else: trade_fx_rate = row[spot_fx_rate_idx] trade_ask_fx_rate = row[ask_fx_rate_idx] trade_bid_fx_rate = row[bid_fx_rate_idx] # update trading info rate_open = trade_ask_fx_rate # use ask price to buy -> Transaction Cost current_pos = equity * leverage # calculate unrealized pnl, use bid price to sell -> Transaction Cost unreal_pnl = calculate_pnl(leverage=leverage, r_foreign=trade_r_f, r_domestic=trade_r_d, rate_open=rate_open, rate_close=trade_bid_fx_rate, trade_period=trade_period) # record trading info record_trade(trade_df=trades, idx=index, signal=max_signal, fx_name=trade_fx, period_name=trade_period_name, foreign_ir=trade_r_f, domestic_ir=trade_r_d, fx_rate=trade_fx_rate, equity=equity, position=current_pos, unreal_r=unreal_pnl, real_r=real_pnl, drawdown=max_drawdown) else: if jpy == 1: # invest in local interest market if no fx signal appears # record trading fx name and period trade_fx = 'JPY' trade_period = 7 trade_period_name = '1W' # record trading day start_day = index end_day = holiday_adjust(start_day, dt.timedelta(days=trade_period)) # Interest rates idx [ fx_libor_idx, dstc_libor_idx, spot_fx_rate_idx, forward_fx_rate_idx, ask_fx_rate_idx, bid_fx_rate_idx ] = cal_rates_name(fx_name=trade_fx, period_name=trade_period_name) # Interest rates trade_r_f = row[fx_libor_idx] / 100 trade_r_d = row[dstc_libor_idx] / 100 if trade_fx == 'JPY': trade_fx_rate = 1 trade_ask_fx_rate = 1 trade_bid_fx_rate = 1 else: trade_fx_rate = row[spot_fx_rate_idx] trade_ask_fx_rate = row[ask_fx_rate_idx] trade_bid_fx_rate = row[bid_fx_rate_idx] if trade_r_d > 0: # invest only when the local interest rate > 0 # update trading info rate_open = trade_ask_fx_rate # use ask price to buy -> Transaction Cost current_pos = equity # calculate unrealized pnl, use bid price to sell -> Transaction Cost unreal_pnl = calculate_pnl( leverage=1, r_foreign=trade_r_f, r_domestic=trade_r_d, rate_open=rate_open, rate_close=trade_bid_fx_rate, trade_period=trade_period) # record trading info record_trade(trade_df=trades, idx=index, signal=max_signal, fx_name=trade_fx, period_name=trade_period_name, foreign_ir=trade_r_f, domestic_ir=trade_r_d, fx_rate=trade_fx_rate, equity=equity, position=current_pos, unreal_r=unreal_pnl, real_r=real_pnl, drawdown=max_drawdown) else: # record trading info record_trade(trade_df=trades, idx=index, signal=max_signal, fx_name='-', period_name='-', foreign_ir=0, domestic_ir=0, fx_rate=1, equity=equity, position=0, unreal_r=unreal_pnl, real_r=real_pnl, drawdown=max_drawdown) else: # record trading info record_trade(trade_df=trades, idx=index, signal=max_signal, fx_name='-', period_name='-', foreign_ir=0, domestic_ir=0, fx_rate=1, equity=equity, position=0, unreal_r=unreal_pnl, real_r=real_pnl, drawdown=max_drawdown) else: # position != 0 # Interest rates idx [ fx_libor_idx, dstc_libor_idx, spot_fx_rate_idx, forward_fx_rate_idx, ask_fx_rate_idx, bid_fx_rate_idx ] = cal_rates_name(fx_name=trade_fx, period_name=trade_period_name) # Interest rates trade_r_f = row[fx_libor_idx] / 100 trade_r_d = row[dstc_libor_idx] / 100 if trade_fx == 'JPY': trade_fx_rate = 1 trade_ask_fx_rate = 1 trade_bid_fx_rate = 1 else: trade_fx_rate = row[spot_fx_rate_idx] trade_ask_fx_rate = row[ask_fx_rate_idx] trade_bid_fx_rate = row[bid_fx_rate_idx] # close the position, return the money we borrowed if index >= end_day: # calculate pnl, use bid price to sell -> Transaction Cost unreal_pnl = 0 temp_pnl = calculate_pnl(leverage=leverage, r_foreign=trade_r_f, r_domestic=trade_r_d, rate_open=rate_open, rate_close=trade_bid_fx_rate, trade_period=trade_period) real_pnl = (1 + real_pnl) * (1 + temp_pnl) - 1 equity *= (1 + temp_pnl) # record drawdown max_drawdown = real_pnl - max_return # update max return if real_pnl > max_return: max_return = real_pnl # record trading info record_trade(trade_df=trades, idx=index, signal=max_signal, fx_name=trade_fx, period_name=trade_period_name, foreign_ir=trade_r_f, domestic_ir=trade_r_d, fx_rate=trade_fx_rate, equity=equity, position=current_pos, unreal_r=unreal_pnl, real_r=real_pnl, drawdown=max_drawdown) # refresh the variables current_pos = 0 rate_open = 0 # refresh trade info trade_fx = '-' trade_period = 0 trade_period_name = '-' else: # calculate unrealized pnl, use bid price to sell -> Transaction Cost unreal_pnl = calculate_pnl(leverage=leverage, r_foreign=trade_r_f, r_domestic=trade_r_d, rate_open=rate_open, rate_close=trade_bid_fx_rate, trade_period=trade_period) # record trading info record_trade(trade_df=trades, idx=index, signal=max_signal, fx_name=trade_fx, period_name=trade_period_name, foreign_ir=trade_r_f, domestic_ir=trade_r_d, fx_rate=trade_fx_rate, equity=equity, position=current_pos, unreal_r=unreal_pnl, real_r=real_pnl, drawdown=max_drawdown) log_message('Algo run complete.') return trades
def algo_loop(trading_day, last_day): log_message('Beginning Tick Strategy run') round_lot = 100 #NBBO NBBO_last_day = last_day[(last_day.qu_source == 'N') & (last_day.natbbo_ind == 4)] avg_spread = (NBBO_last_day.ask_px - NBBO_last_day.bid_px).mean() half_spread = avg_spread / 2 print("Average stock spread for sample: {:.4f}".format(avg_spread)) # init our price and volume variables [last_price, last_size, bid_price, bid_size, ask_price, ask_size, volume] = np.zeros(7) # init our counters [trade_count, quote_count, cumulative_volume] = [0, 0, 0] # init some time series objects for collection of telemetry fair_values = pd.Series(index=trading_day.index) midpoints = pd.Series(index=trading_day.index) tick_factors = pd.Series(index=trading_day.index) # let's set up a container to hold trades. preinitialize with the index trades = pd.DataFrame( columns=['price', 'shares', 'bar', 'trade_type', 'side'], index=trading_day.index) # MAIN EVENT LOOP current_bar = 0 # track state and values for a current working order live_order = False live_order_price = 0.0 live_order_quantity = 0.0 order_side = '-' # other order and market variables total_quantity_filled = 0 vwap_numerator = 0.0 total_trade_count = 0 total_agg_count = 0 total_pass_count = 0 # fair value pricing variables midpoint = 0.0 fair_value = 0.0 # define our accumulator for the tick EMA message_type = 0 tick_coef = 1 tick_window = 20 tick_factor = 0 tick_ema_alpha = 2 / (tick_window + 1) prev_tick = 0 prev_price = 0 # risk factor for part 2 # TODO: implement and evaluate risk_factor = 0.0 risk_coef = -0.5 n = 2 new_midpoint = 0.0 cache = [[0 for i in range(4)] for i in range(n)] trade_lag = [0 for i in range(3)] log_message('starting main loop') for index, row in trading_day.iterrows(): # Update risk factor # TODO: implement and evaluate if total_quantity_filled > 5000: risk_factor = 1 elif total_quantity_filled <= -5000: risk_factor = -1 else: risk_factor = 0 # get the time of this message time_from_open = (index - pd.Timedelta(hours=9, minutes=30)) minutes_from_open = (time_from_open.hour * 60) + time_from_open.minute # MARKET DATA HANDLING if pd.isna(row.trade_px): # it's a quote # skip if not NBBO if not ((row.qu_source == 'N') and (row.natbbo_ind == 4)): continue # set our local NBBO variables if (row.bid_px > 0 and row.bid_size > 0): bid_price = row.bid_px bid_size = row.bid_size if (row.ask_px > 0 and row.ask_size > 0): ask_price = row.ask_px ask_size = row.ask_size quote_count += 1 message_type = 'q' cache.pop(0) cache.append([message_type, row.bid_px, row.ask_px, row.trade_px]) else: # it's a trade # store the last trade price prev_price = last_price # now get the new data last_price = row.trade_px last_size = row.trade_size trade_count += 1 cumulative_volume += row.trade_size vwap_numerator += last_size * last_price message_type = 't' cache.pop(0) cache.append([message_type, row.bid_px, row.ask_px, row.trade_px]) trade_lag.pop(0) trade_lag.append(row.trade_px) # CHECK OPEN ORDER(S) if we have a live order, # has it been filled by the trade that just happened? if live_order: if (order_side == 'b') and (last_price <= live_order_price): fill_size = min(live_order_quantity, last_size) record_trade(trades, index, live_order_price, fill_size, current_bar, 'p', order_side) total_quantity_filled += fill_size total_pass_count += 1 # even if we only got partially filled, let's assume we're cancelling the entire quantity. # If we're still behind we'll replace later in the loop live_order = False live_order_price = 0.0 live_order_quantity = 0.0 if (order_side == 's') and (last_price >= live_order_price): fill_size = min(live_order_quantity, last_size) record_trade(trades, index, live_order_price, fill_size, current_bar, 'p', order_side) total_quantity_filled -= fill_size total_pass_count += 1 # even if we only got partially filled, let's assume we're cancelling the entire quantity. # If we're still behind we'll replace later in the loop live_order = False live_order_price = 0.0 live_order_quantity = 0.0 # TICK FACTOR # only update if it's a trade if message_type == 't': # calc the tick this_tick = np.sign(last_price - prev_price) if this_tick == 0: this_tick = prev_tick # now calc the tick if tick_factor == 0: tick_factor = this_tick else: tick_factor = (tick_ema_alpha * this_tick) + (1 - tick_ema_alpha) * tick_factor # store the last tick prev_tick = this_tick # PRICING LOGIC new_midpoint = bid_price + (ask_price - bid_price) / 2 if new_midpoint > 0: midpoint = new_midpoint # Determine the trade's trend using tick test if message_type == 't': flag = 0 temp = cache.copy() temp.reverse() temp.pop(0) while (len(temp) != 1): lag = temp.pop(0) if lag[0] == 't': if last_price > lag[3]: order_side = 'b' if last_price < lag[3]: order_side = 's' if last_price == lag[3]: if trade_lag[0] < lag[3]: order_side = 'b' if trade_lag[0] > lag[3]: order_side = 's' else: order_side = '-' flag = 1 break # Determine the trade's trend using quote (if available, flag = 0) if flag == 0 and temp[0][0] == 'q': midpoint = temp[0][1] + (temp[0][2] - temp[0][1]) / 2 if last_price > midpoint: order_side = 'b' if last_price < midpoint: order_side = 's' if last_price == midpoint: if last_price > trade_lag[1]: order_side = 'b' if last_price < trade_lag[1]: order_side = 's' if last_price == trade_lag[1]: if trade_lag[0] < trade_lag[1]: order_side = 'b' if trade_lag[0] > trade_lag[1]: order_side = 's' else: order_side = '-' # FAIR VALUE CALCULATION(when me meet the trade message) fair_value = midpoint + half_spread * ((tick_coef * tick_factor) + (risk_coef * risk_factor)) # collect our data fair_values[index] = fair_value midpoints[index] = midpoint tick_factors[index] = tick_factor # TRADE DECISION # TODO: determine if we want to buy or sell # if fair price is < bid, sell agg for all bid price and amount # if fair price is < ask, sell passive # if fair price is > ask, buy agg for all ask price and amount # if fair price is > bid, buy passive # TRADING LOGIC # check where our FV is versus the BBO and constrain if message_type == 'q': if order_side == 'b': if fair_value >= ask_price: total_agg_count += 1 new_trade_price = ask_price # now place our aggressive order: assume you can execute the full size across spread new_order_quantity = ask_size record_trade(trades, index, new_trade_price, new_order_quantity, current_bar, 'a', order_side) # update quantity remaining total_quantity_filled += new_order_quantity live_order_quantity = 0.0 live_order_price = 0.0 live_order = False else: # we're not yet willing to cross the spread, stay passive live_order_price = bid_price live_order_quantity = ask_size live_order = True elif order_side == 's': if fair_value <= bid_price: total_agg_count += 1 new_trade_price = bid_price # now place our aggressive order: assume you can execute the full size across spread new_order_quantity = bid_size # new_order_quantity = quantity_behind record_trade(trades, index, new_trade_price, new_order_quantity, current_bar, 'a', order_side) # update quantity remaining total_quantity_filled -= new_order_quantity live_order_quantity = 0.0 live_order_price = 0.0 live_order = False else: # not yet willing to cross spread live_order_price = ask_price live_order_quantity = bid_size live_order = True else: # no order here. for now just continue continue # looping done log_message('end simulation loop') log_message('order analytics') # Now, let's look at some stats trades = trades.dropna() day_vwap = vwap_numerator / cumulative_volume # prep our text output avg_price = 0 if trades['shares'].sum() != 0: avg_price = (trades['price'] * trades['shares']).sum() / trades['shares'].sum() log_message('Algo run complete.') # assemble results and return # TODO: add P&L return { 'midpoints': midpoints, 'fair_values': fair_values, 'tick_factors': tick_factors, 'trades': trades, 'quote_count': quote_count, 'day_vwap': day_vwap, 'avg_price': avg_price }