Overall Statistics
class CustomModelsAlgorithm(QCAlgorithm):
    '''Demonstration of using custom fee, slippage, fill, and buying power models for modelling transactions in backtesting.
    QuantConnect allows you to model all orders as deeply and accurately as you need.'''

    def Initialize(self):
        self.SetStartDate(2013,10,1)   # Set Start Date
        self.SetEndDate(2013,10,31)    # Set End Date
        self.security = self.AddEquity("SPY", Resolution.Hour)
        self.spy = self.security.Symbol

        # set our models
        self.security.SetFeeModel(CustomFeeModel(self))
        self.security.SetFillModel(CustomFillModel(self))
        self.security.SetSlippageModel(CustomSlippageModel(self))
        self.security.SetBuyingPowerModel(CustomBuyingPowerModel(self))


    def OnData(self, data):
        open_orders = self.Transactions.GetOpenOrders(self.spy)
        if len(open_orders) != 0: return

        if self.Time.day > 10 and self.security.Holdings.Quantity <= 0:
            quantity = self.CalculateOrderQuantity(self.spy, .5)
            self.Log(f"MarketOrder: {quantity}")
            self.MarketOrder(self.spy, quantity, True)   # async needed for partial fill market orders

        elif self.Time.day > 20 and self.security.Holdings.Quantity >= 0:
            quantity = self.CalculateOrderQuantity(self.spy, -.5)
            self.Log(f"MarketOrder: {quantity}")
            self.MarketOrder(self.spy, quantity, True)   # async needed for partial fill market orders

# If we want to use methods from other models, you need to inherit from one of them
class CustomFillModel(ImmediateFillModel):
    def __init__(self, algorithm):
        self.algorithm = algorithm
        self.absoluteRemainingByOrderId = {}
        self.random = Random(387510346)

    def MarketFill(self, asset, order):
        absoluteRemaining = order.AbsoluteQuantity

        if order.Id in self.absoluteRemainingByOrderId.keys():
            absoluteRemaining = self.absoluteRemainingByOrderId[order.Id]

        fill = super().MarketFill(asset, order)
        absoluteFillQuantity = int(min(absoluteRemaining, self.random.Next(0, 2*int(order.AbsoluteQuantity))))
        fill.FillQuantity = np.sign(order.Quantity) * absoluteFillQuantity
        
        if absoluteRemaining == absoluteFillQuantity:
            fill.Status = OrderStatus.Filled
            if self.absoluteRemainingByOrderId.get(order.Id):
                self.absoluteRemainingByOrderId.pop(order.Id)
        else:
            absoluteRemaining = absoluteRemaining - absoluteFillQuantity
            self.absoluteRemainingByOrderId[order.Id] = absoluteRemaining
            fill.Status = OrderStatus.PartiallyFilled
        self.algorithm.Log(f"CustomFillModel: {fill}")
        return fill

class CustomFeeModel(FeeModel):
    def __init__(self, algorithm):
        self.algorithm = algorithm

    def GetOrderFee(self, parameters):
        # custom fee math
        fee = max(1, parameters.Security.Price
                  * parameters.Order.AbsoluteQuantity
                  * 0.00001)
        self.algorithm.Log(f"CustomFeeModel: {fee}")
        return OrderFee(CashAmount(fee, "USD"))

class CustomSlippageModel:
    def __init__(self, algorithm):
        self.algorithm = algorithm

    def GetSlippageApproximation(self, asset, order):
        # custom slippage math
        slippage = asset.Price * 0.0001 * np.log10(2*float(order.AbsoluteQuantity))
        self.algorithm.Log(f"CustomSlippageModel: {slippage}")
        return slippage

class CustomBuyingPowerModel(BuyingPowerModel):
    def __init__(self, algorithm):
        self.algorithm = algorithm

    def HasSufficientBuyingPowerForOrder(self, parameters):
        # custom behavior: this model will assume that there is always enough buying power
        hasSufficientBuyingPowerForOrderResult = HasSufficientBuyingPowerForOrderResult(True)
        self.algorithm.Log(f"CustomBuyingPowerModel: {hasSufficientBuyingPowerForOrderResult.IsSufficient}")
        return hasSufficientBuyingPowerForOrderResult