Skip to content

Commit

Permalink
PEP8 style algorithm API (#7909)
Browse files Browse the repository at this point in the history
* feat: support snake-case style Python QCAlgorithm implementations

* feat: add unit tests and minor fixes

* feat: implement new BasePythonWrapper class for python wrappers.

Used to cache methods and contains invoke functionality

* feat: make python wrappers implement the new base class for pep8 style support

* feat: keep overriden methods in Algorithm Python Wrapper

* feat: add unit tests for custom models algorithms with PEP8 style

* Bump pythonnet version to 2.0.30

* fix bugs and address peer review

* Address peer review

* Minor revert

* feat: StubsIgnoreAttribute for ignoring members or classes by the stubs generator

* Minor fixes

* Minor fix

* Minor fix

* Bump pythonnet version to 2.0.31

* Added Greeks.Lambda_ alias of Lambda for python compatibility.

Remove unused method
  • Loading branch information
jhonabreul authored Apr 12, 2024
1 parent 90f09a1 commit 2ddf40b
Show file tree
Hide file tree
Showing 55 changed files with 1,101 additions and 658 deletions.
4 changes: 2 additions & 2 deletions Algorithm.CSharp/QuantConnect.Algorithm.CSharp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
<DebugType>portable</DebugType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="QuantConnect.pythonnet" Version="2.0.29" />
<PackageReference Include="QuantConnect.pythonnet" Version="2.0.31" />
<PackageReference Include="Accord" Version="3.6.0" />
<PackageReference Include="Accord.Fuzzy" Version="3.6.0" />
<PackageReference Include="Accord.MachineLearning" Version="3.6.0" />
Expand All @@ -60,4 +60,4 @@
<PackagePath></PackagePath>
</None>
</ItemGroup>
</Project>
</Project>
11 changes: 3 additions & 8 deletions Algorithm.Framework/Portfolio/PortfolioOptimizerPythonWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,15 @@ namespace QuantConnect.Algorithm.Framework.Portfolio
/// <summary>
/// Python wrapper for custom portfolio optimizer
/// </summary>
public class PortfolioOptimizerPythonWrapper : IPortfolioOptimizer
public class PortfolioOptimizerPythonWrapper : BasePythonWrapper<IPortfolioOptimizer>, IPortfolioOptimizer
{
private readonly dynamic _portfolioOptimizer;

/// <summary>
/// Creates a new instance
/// </summary>
/// <param name="portfolioOptimizer">The python model to wrapp</param>
public PortfolioOptimizerPythonWrapper(PyObject portfolioOptimizer)
: base(portfolioOptimizer)
{
_portfolioOptimizer = portfolioOptimizer.ValidateImplementationOf<IPortfolioOptimizer>();
}

/// <summary>
Expand All @@ -44,10 +42,7 @@ public PortfolioOptimizerPythonWrapper(PyObject portfolioOptimizer)
/// <returns>Array of double with the portfolio weights (size: K x 1)</returns>
public double[] Optimize(double[,] historicalReturns, double[] expectedReturns = null, double[,] covariance = null)
{
using (Py.GIL())
{
return _portfolioOptimizer.Optimize(historicalReturns, expectedReturns, covariance);
}
return InvokeMethod<double[]>(nameof(Optimize), historicalReturns, expectedReturns, covariance);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
<PackageLicenseFile>LICENSE</PackageLicenseFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="QuantConnect.pythonnet" Version="2.0.29" />
<PackageReference Include="QuantConnect.pythonnet" Version="2.0.31" />
<PackageReference Include="Accord" Version="3.6.0" />
<PackageReference Include="Accord.Math" Version="3.6.0" />
<PackageReference Include="Accord.Statistics" Version="3.6.0" />
Expand Down
178 changes: 178 additions & 0 deletions Algorithm.Python/CustomModelsPEP8Algorithm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
# 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 *
import random

### <summary>
### Demonstration of using custom fee, slippage, fill, and buying power models for modeling transactions in backtesting.
### QuantConnect allows you to model all orders as deeply and accurately as you need.
### This example illustrates how Lean exports its API to Python conforming to PEP8 style guide.
### </summary>
### <meta name="tag" content="trading and orders" />
### <meta name="tag" content="transaction fees and slippage" />
### <meta name="tag" content="custom buying power models" />
### <meta name="tag" content="custom transaction models" />
### <meta name="tag" content="custom slippage models" />
### <meta name="tag" content="custom fee models" />
class CustomModelsPEP8Algorithm(QCAlgorithm):
'''Demonstration of using custom fee, slippage, fill, and buying power models for modeling transactions in backtesting.
QuantConnect allows you to model all orders as deeply and accurately as you need.'''

def initialize(self):
self.set_start_date(2013,10,1) # Set Start Date
self.set_end_date(2013,10,31) # Set End Date
self.security = self.add_equity("SPY", Resolution.HOUR)
self.spy = self.security.symbol

# set our models
self.security.set_fee_model(CustomFeeModelPEP8(self))
self.security.set_fill_model(CustomFillModelPEP8(self))
self.security.set_slippage_model(CustomSlippageModelPEP8(self))
self.security.set_buying_power_model(CustomBuyingPowerModelPEP8(self))

def on_data(self, data):
open_orders = self.transactions.get_open_orders(self.spy)
if len(open_orders) != 0: return
if self.time.day > 10 and self.security.holdings.quantity <= 0:
quantity = self.calculate_order_quantity(self.spy, .5)
self.log(f"MarketOrder: {quantity}")
self.market_order(self.spy, quantity, True) # async needed for partial fill market orders
elif self.time.day > 20 and self.security.holdings.quantity >= 0:
quantity = self.calculate_order_quantity(self.spy, -.5)
self.log(f"MarketOrder: {quantity}")
self.market_order(self.spy, quantity, True) # async needed for partial fill market orders

class CustomFillModelPEP8(ImmediateFillModel):
def __init__(self, algorithm):
super().__init__()
self.algorithm = algorithm
self.absolute_remaining_by_order_id = {}
self.random = Random(387510346)

def market_fill(self, asset, order):
absolute_remaining = order.absolute_quantity

if order.id in self.absolute_remaining_by_order_id.keys():
absolute_remaining = self.absolute_remaining_by_order_id[order.id]

fill = super().market_fill(asset, order)
absolute_fill_quantity = int(min(absolute_remaining, self.random.next(0, 2*int(order.absolute_quantity))))
fill.fill_quantity = np.sign(order.quantity) * absolute_fill_quantity

if absolute_remaining == absolute_fill_quantity:
fill.status = OrderStatus.FILLED
if self.absolute_remaining_by_order_id.get(order.id):
self.absolute_remaining_by_order_id.pop(order.id)
else:
absolute_remaining = absolute_remaining - absolute_fill_quantity
self.absolute_remaining_by_order_id[order.id] = absolute_remaining
fill.status = OrderStatus.PARTIALLY_FILLED
self.algorithm.log(f"CustomFillModel: {fill}")
return fill

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

def get_order_fee(self, parameters):
# custom fee math
fee = max(1, parameters.security.price
* parameters.order.absolute_quantity
* 0.00001)
self.algorithm.log(f"CustomFeeModel: {fee}")
return OrderFee(CashAmount(fee, "USD"))

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

def get_slippage_approximation(self, asset, order):
# custom slippage math
slippage = asset.price * 0.0001 * np.log10(2*float(order.absolute_quantity))
self.algorithm.log(f"CustomSlippageModel: {slippage}")
return slippage

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

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

class SimpleCustomFillModelPEP8(FillModel):
def __init__(self):
super().__init()

def _create_order_event(self, asset, order):
utc_time = Extensions.convert_to_utc(asset.local_time, asset.exchange.time_zone)
return OrderEvent(order, utc_time, OrderFee.zero)

def _set_order_event_to_filled(self, fill, fill_price, fill_quantity):
fill.status = OrderStatus.FILLED
fill.fill_quantity = fill_quantity
fill.fill_price = fill_price
return fill

def _get_trade_bar(self, asset, order_direction):
trade_bar = asset.cache.get_data[TradeBar]()
if trade_bar:
return trade_bar

price = asset.price
return TradeBar(asset.local_time, asset.symbol, price, price, price, price, 0)

def market_fill(self, asset, order):
fill = self._create_order_event(asset, order)
if order.status == OrderStatus.CANCELED:
return fill

fill_price = asset.cache.ask_price if order.direction == OrderDirection.BUY else asset.cache.bid_price
return self._set_order_event_to_filled(fill, fill_price, order.quantity)

def stop_market_fill(self, asset, order):
fill = self._create_order_event(asset, order)
if order.status == OrderStatus.CANCELED:
return fill

stop_price = order.stop_price
trade_bar = self._get_trade_bar(asset, order.direction)

if order.direction == OrderDirection.SELL and trade_bar.low < stop_price:
return self._set_order_event_to_filled(fill, stop_price, order.quantity)

if order.direction == OrderDirection.BUY and trade_bar.high > stop_price:
return self._set_order_event_to_filled(fill, stop_price, order.quantity)

return fill

def limit_fill(self, asset, order):
fill = self._create_order_event(asset, order)
if order.status == OrderStatus.CANCELED:
return fill

limit_price = order.limit_price
trade_bar = self._get_trade_bar(asset, order.direction)

if order.direction == OrderDirection.SELL and trade_bar.high > limit_price:
return self._set_order_event_to_filled(fill, limit_price, order.quantity)

if order.direction == OrderDirection.BUY and trade_bar.low < limit_price:
return self._set_order_event_to_filled(fill, limit_price, order.quantity)

return fill
10 changes: 5 additions & 5 deletions Algorithm.Python/FundamentalRegressionAlgorithm.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def Initialize(self):

self.UniverseSettings.Resolution = Resolution.Daily

self.universe = self.AddUniverse(self.SelectionFunction)
self._universe = self.AddUniverse(self.SelectionFunction)

# before we add any symbol
self.AssertFundamentalUniverseData()
Expand Down Expand Up @@ -69,25 +69,25 @@ def Initialize(self):

def AssertFundamentalUniverseData(self):
# Case A
universeDataPerTime = self.History(self.universe.DataType, [self.universe.Symbol], TimeSpan(2, 0, 0, 0))
universeDataPerTime = self.History(self._universe.DataType, [self._universe.Symbol], TimeSpan(2, 0, 0, 0))
if len(universeDataPerTime) != 2:
raise ValueError(f"Unexpected Fundamentals history count {len(universeDataPerTime)}! Expected 2")

for universeDataCollection in universeDataPerTime:
self.AssertFundamentalEnumerator(universeDataCollection, "A")

# Case B (sugar on A)
universeDataPerTime = self.History(self.universe, TimeSpan(2, 0, 0, 0))
universeDataPerTime = self.History(self._universe, TimeSpan(2, 0, 0, 0))
if len(universeDataPerTime) != 2:
raise ValueError(f"Unexpected Fundamentals history count {len(universeDataPerTime)}! Expected 2")

for universeDataCollection in universeDataPerTime:
self.AssertFundamentalEnumerator(universeDataCollection, "B")

# Case C: Passing through the unvierse type and symbol
enumerableOfDataDictionary = self.History[self.universe.DataType]([self.universe.Symbol], 100)
enumerableOfDataDictionary = self.History[self._universe.DataType]([self._universe.Symbol], 100)
for selectionCollectionForADay in enumerableOfDataDictionary:
self.AssertFundamentalEnumerator(selectionCollectionForADay[self.universe.Symbol], "C")
self.AssertFundamentalEnumerator(selectionCollectionForADay[self._universe.Symbol], "C")

def AssertFundamentalEnumerator(self, enumerable, caseName):
dataPointCount = 0
Expand Down
48 changes: 48 additions & 0 deletions Algorithm.Python/PEP8StyleBasicAlgorithm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# 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 PEP8StyleBasicAlgorithm(QCAlgorithm):

def initialize(self):
self.set_start_date(2013,10, 7)
self.set_end_date(2013,10,11)
self.set_cash(100000)

self.spy = self.add_equity("SPY", Resolution.MINUTE, extended_market_hours=False, fill_forward=True).symbol

# Test accessing a constant (QCAlgorithm.MaxTagsCount)
self.debug("MaxTagsCount: " + str(self.MAX_TAGS_COUNT))

def on_data(self, slice):
if not self.portfolio.invested:
self.set_holdings(self.spy, 1)
self.debug("Purchased Stock")

def on_order_event(self, order_event):
self.log(f"{self.time} :: {order_event}")

def on_end_of_algorithm(self):
self.log("Algorithm ended!")

if not self.portfolio.invested:
raise Exception("Algorithm should have been invested at the end of the algorithm")

# let's do some logging to do more pep8 style testing
self.log("-----------------------------------------------------------------------------------------")
self.log(f"{self.spy.value} last price: {self.securities[self.spy].price}")
self.log(f"{self.spy.value} holdings: "
f"{self.securities[self.spy].holdings.quantity}@{self.securities[self.spy].holdings.price}="
f"{self.securities[self.spy].holdings.holdings_value}")
self.log("-----------------------------------------------------------------------------------------")
4 changes: 2 additions & 2 deletions Algorithm.Python/QuantConnect.Algorithm.Python.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
<Compile Include="..\Common\Properties\SharedAssemblyInfo.cs" Link="Properties\SharedAssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="QuantConnect.pythonnet" Version="2.0.29" />
<PackageReference Include="QuantConnect.pythonnet" Version="2.0.31" />
</ItemGroup>
<ItemGroup>
<Content Include="FundamentalUniverseSelectionAlgorithm.py" />
Expand Down Expand Up @@ -276,4 +276,4 @@
</PostBuildEvent>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
</PropertyGroup>
</Project>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def Initialize(self):
self.SetEndDate(2014, 3, 26)
self.SetCash(100000)

self.securities = []
self._securities = []
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.select_symbol)

Expand All @@ -49,8 +49,8 @@ def select_symbol(self, fundamental):
return security_ids

def OnSecuritiesChanged(self, changes):
self.securities.extend(changes.AddedSecurities)
self._securities.extend(changes.AddedSecurities)

def OnEndOfAlgorithm(self):
if not self.securities:
if not self._securities:
raise Exception("No securities were selected")
10 changes: 5 additions & 5 deletions Algorithm.Python/UniverseSelectedRegressionAlgorithm.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def Initialize(self):

self.UniverseSettings.Resolution = Resolution.Daily

self.universe = self.AddUniverse(self.SelectionFunction)
self._universe = self.AddUniverse(self.SelectionFunction)
self.selectionCount = 0

def SelectionFunction(self, fundamentals):
Expand All @@ -37,13 +37,13 @@ def SelectionFunction(self, fundamentals):
return [ x.Symbol for x in sortedByDollarVolume[:self.selectionCount] ]

def OnData(self, data):
if Symbol.Create("TSLA", SecurityType.Equity, Market.USA) in self.universe.Selected:
if Symbol.Create("TSLA", SecurityType.Equity, Market.USA) in self._universe.Selected:
raise ValueError(f"TSLA shouldn't of been selected")

self.Buy(next(iter(self.universe.Selected)), 1)
self.Buy(next(iter(self._universe.Selected)), 1)

def OnEndOfAlgorithm(self):
if self.selectionCount != 3:
raise ValueError(f"Unexpected selection count {self.selectionCount}")
if self.universe.Selected.Count != 3 or self.universe.Selected.Count == self.universe.Members.Count:
raise ValueError(f"Unexpected universe selected count {self.universe.Selected.Count}")
if self._universe.Selected.Count != 3 or self._universe.Selected.Count == self._universe.Members.Count:
raise ValueError(f"Unexpected universe selected count {self._universe.Selected.Count}")
Loading

0 comments on commit 2ddf40b

Please sign in to comment.