chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
|
||||
# Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from AlgorithmImports import *
|
||||
|
||||
class VolumeWeightedAveragePriceExecutionModel(ExecutionModel):
|
||||
'''Execution model that submits orders while the current market price is more favorable that the current volume weighted average price.'''
|
||||
|
||||
def __init__(self, asynchronous=True):
|
||||
'''Initializes a new instance of the VolumeWeightedAveragePriceExecutionModel class'''
|
||||
super().__init__(asynchronous)
|
||||
self.targets_collection = PortfolioTargetCollection()
|
||||
self.symbol_data = {}
|
||||
|
||||
# Gets or sets the maximum order quantity as a percentage of the current bar's volume.
|
||||
# This defaults to 0.01m = 1%. For example, if the current bar's volume is 100,
|
||||
# then the maximum order size would equal 1 share.
|
||||
self.maximum_order_quantity_percent_volume = 0.01
|
||||
|
||||
|
||||
def execute(self, algorithm, targets):
|
||||
'''Executes market orders if the standard deviation of price is more
|
||||
than the configured number of deviations in the favorable direction.
|
||||
Args:
|
||||
algorithm: The algorithm instance
|
||||
targets: The portfolio targets'''
|
||||
# update the complete set of portfolio targets with the new targets
|
||||
self.targets_collection.add_range(targets)
|
||||
|
||||
# for performance we check count value, OrderByMarginImpact and ClearFulfilled are expensive to call
|
||||
if not self.targets_collection.is_empty:
|
||||
for target in self.targets_collection.order_by_margin_impact(algorithm):
|
||||
symbol = target.symbol
|
||||
|
||||
# calculate remaining quantity to be ordered
|
||||
unordered_quantity = OrderSizing.get_unordered_quantity(algorithm, target)
|
||||
|
||||
# fetch our symbol data containing our VWAP indicator
|
||||
data = self.symbol_data.get(symbol, None)
|
||||
if data is None: return
|
||||
|
||||
# check order entry conditions
|
||||
if self.price_is_favorable(data, unordered_quantity):
|
||||
# adjust order size to respect maximum order size based on a percentage of current volume
|
||||
order_size = OrderSizing.get_order_size_for_percent_volume(data.security, self.maximum_order_quantity_percent_volume, unordered_quantity)
|
||||
|
||||
if order_size != 0:
|
||||
algorithm.market_order(symbol, order_size, self.asynchronous, target.tag)
|
||||
|
||||
self.targets_collection.clear_fulfilled(algorithm)
|
||||
|
||||
|
||||
def on_securities_changed(self, algorithm, changes):
|
||||
'''Event fired each time the we add/remove securities from the data feed
|
||||
Args:
|
||||
algorithm: The algorithm instance that experienced the change in securities
|
||||
changes: The security additions and removals from the algorithm'''
|
||||
for removed in changes.removed_securities:
|
||||
# clean up removed security data
|
||||
if removed.symbol in self.symbol_data:
|
||||
if self.is_safe_to_remove(algorithm, removed.symbol):
|
||||
data = self.symbol_data.pop(removed.symbol)
|
||||
algorithm.subscription_manager.remove_consolidator(removed.symbol, data.consolidator)
|
||||
|
||||
for added in changes.added_securities:
|
||||
if added.symbol not in self.symbol_data:
|
||||
self.symbol_data[added.symbol] = SymbolData(algorithm, added)
|
||||
|
||||
|
||||
def price_is_favorable(self, data, unordered_quantity):
|
||||
'''Determines if the current price is more than the configured
|
||||
number of standard deviations away from the mean in the favorable direction.'''
|
||||
if unordered_quantity > 0:
|
||||
if data.security.bid_price < data.vwap:
|
||||
return True
|
||||
else:
|
||||
if data.security.ask_price > data.vwap:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def is_safe_to_remove(self, algorithm, symbol):
|
||||
'''Determines if it's safe to remove the associated symbol data'''
|
||||
# confirm the security isn't currently a member of any universe
|
||||
return not any([kvp.value.contains_member(symbol) for kvp in algorithm.universe_manager])
|
||||
|
||||
class SymbolData:
|
||||
def __init__(self, algorithm, security):
|
||||
self.security = security
|
||||
self.consolidator = algorithm.resolve_consolidator(security.symbol, security.resolution)
|
||||
name = algorithm.create_indicator_name(security.symbol, "VWAP", security.resolution)
|
||||
self._vwap = IntradayVwap(name)
|
||||
algorithm.register_indicator(security.symbol, self._vwap, self.consolidator)
|
||||
|
||||
@property
|
||||
def vwap(self):
|
||||
return self._vwap.value
|
||||
|
||||
class IntradayVwap:
|
||||
'''Defines the canonical intraday VWAP indicator'''
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self.value = 0.0
|
||||
self.last_date = datetime.min
|
||||
self.sum_of_volume = 0.0
|
||||
self.sum_of_price_times_volume = 0.0
|
||||
|
||||
@property
|
||||
def is_ready(self):
|
||||
return self.sum_of_volume > 0.0
|
||||
|
||||
def update(self, input):
|
||||
'''Computes the new VWAP'''
|
||||
success, volume, average_price = self.get_volume_and_average_price(input)
|
||||
if not success:
|
||||
return self.is_ready
|
||||
|
||||
# reset vwap on daily boundaries
|
||||
if self.last_date != input.end_time.date():
|
||||
self.sum_of_volume = 0.0
|
||||
self.sum_of_price_times_volume = 0.0
|
||||
self.last_date = input.end_time.date()
|
||||
|
||||
# running totals for Σ PiVi / Σ Vi
|
||||
self.sum_of_volume += volume
|
||||
self.sum_of_price_times_volume += average_price * volume
|
||||
|
||||
if self.sum_of_volume == 0.0:
|
||||
# if we have no trade volume then use the current price as VWAP
|
||||
self.value = input.value
|
||||
return self.is_ready
|
||||
|
||||
self.value = self.sum_of_price_times_volume / self.sum_of_volume
|
||||
return self.is_ready
|
||||
|
||||
def get_volume_and_average_price(self, input):
|
||||
'''Determines the volume and price to be used for the current input in the VWAP computation'''
|
||||
|
||||
if type(input) is Tick:
|
||||
if input.tick_type == TickType.TRADE:
|
||||
return True, float(input.quantity), float(input.last_price)
|
||||
|
||||
if type(input) is TradeBar:
|
||||
if not input.is_fill_forward:
|
||||
average_price = float(input.high + input.low + input.close) / 3
|
||||
return True, float(input.volume), average_price
|
||||
|
||||
return False, 0.0, 0.0
|
||||
Reference in New Issue
Block a user