Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding order.get_filled_price() and enabling pytest coverage #267

Merged
merged 3 commits into from
Aug 3, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Adding order.get_filled_price() and enabling coverage to report pytes…
…t unittest coverage.
  • Loading branch information
davidlatte committed Aug 3, 2023
commit 171574507ee19287c9d0fc779227b240699d1064
18 changes: 18 additions & 0 deletions lumibot/entities/order.py
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,24 @@ def cash_pending(self, strategy):
else:
return -cash_value

def get_filled_price(self) -> float:
davidlatte marked this conversation as resolved.
Show resolved Hide resolved
"""
Get the weighted average filled price for this order. Option contracts often encounter partial fills,
so the weighted average is the only valid price that can be used for PnL calculations.

Returns
-------
float
The weighted average filled price for this order. 0.0 will be returned if the order
has not yet been filled.
"""
# Only calculate on filled orders
if not self.transactions or not self.position_filled:
return 0.0

# calculate the weighted average filled price since options often encounter partial fills
return round(sum([x.price * x.quantity for x in self.transactions]) / self.quantity, 2)

def update_status(self, status):
self.status = status

Expand Down
68 changes: 35 additions & 33 deletions lumibot/entities/trading_fee.py
davidlatte marked this conversation as resolved.
Show resolved Hide resolved
davidlatte marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,43 @@


class TradingFee:
"""TradingFee class. Used to define the trading fees for a broker in a strategy/backtesting.
"""TradingFee class. Used to define the trading fees for a broker in a strategy/backtesting."""

Parameters
----------
flat_fee : Decimal, float, or None
Flat fee to pay for each order. This is a fixed fee that is paid for each order in the quote currency.
percentage_fee : Decimal, float, or None
Percentage fee to pay for each order. This is a percentage of the order value that is paid for each order in the quote currency.
maker : bool
Whether this fee is a maker fee (applies to limit orders).
Default is True, which means that this fee will be used on limit orders.
taker : bool
Whether this fee is a taker fee (applies to market orders).
Default is True, which means that this fee will be used on market orders.
def __init__(self, flat_fee=0.0, percent_fee=0.0, maker=True, taker=True):
"""
Parameters
----------
flat_fee : Decimal, float, or None
Flat fee to pay for each order. This is a fixed fee that is paid for each order in the quote currency.
percent_fee : Decimal, float, or None
Percentage fee to pay for each order. This is a percentage of the order value that is paid for each order in the quote currency.
maker : bool
Whether this fee is a maker fee (applies to limit orders).
Default is True, which means that this fee will be used on limit orders.
taker : bool
Whether this fee is a taker fee (applies to market orders).
Default is True, which means that this fee will be used on market orders.

Example
--------
>>> trading_fee_1 = TradingFee(flat_fee=5) # $5 flat fee
>>> trading_fee_2 = TradingFee(percent_fee=0.01) # 1% fee
>>> backtesting_start = datetime(2022, 1, 1)
>>> backtesting_end = datetime(2022, 6, 1)
>>> result = MyStrategy.backtest(
>>> YahooDataBacktesting,
>>> backtesting_start,
>>> backtesting_end,
>>> buy_trading_fees=[trading_fee_1, trading_fee_2],
>>> )
"""

flat_fee: Decimal = 0
percent_fee: Decimal = 0
maker: bool = False
taker: bool = False

def __init__(self, flat_fee=0, percent_fee=0, maker=True, taker=True):
Example
--------
>>> from datetime import datetime
>>> from lumibot.entities import TradingFee
>>> from lumibot.strategies import Strategy
>>> from lumibot.backtesting import YahooDataBacktesting
>>> class MyStrategy(Strategy):
>>> pass
>>>
>>> trading_fee_1 = TradingFee(flat_fee=5.2) # $5.20 flat fee
>>> trading_fee_2 = TradingFee(percent_fee=0.01) # 1% fee
>>> backtesting_start = datetime(2022, 1, 1)
>>> backtesting_end = datetime(2022, 6, 1)
>>> result = MyStrategy.backtest(
>>> YahooDataBacktesting,
>>> backtesting_start,
>>> backtesting_end,
>>> buy_trading_fees=[trading_fee_1, trading_fee_2],
>>> )
"""
self.flat_fee = Decimal(flat_fee)
self.percent_fee = Decimal(percent_fee)
self.maker = maker
Expand Down
37 changes: 37 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Additional items here based on your project needs.
# How this is different from setup.py is ... complicated.
# StackOverflow Answer: https://stackoverflow.com/a/33685899
#
# The short version is that setup.py is for installing the package
# and requirements.txt is for installing the dependencies and that these are
# meant for different audiences. So we duplicate the lists of modules :(
#
# More Long Answer:
# https://towardsdatascience.com/requirements-vs-setuptools-python-ae3ee66e28af

### ----- Requirements for the package ----- ###
polygon==1.1.0
alpaca_trade_api==2.3.0
alpha_vantage
ibapi==9.81.1.post1
yfinance>=0.2.18
matplotlib>=3.3.3
quandl
pandas>=1.4.0<2.0.0 # pandas v2 currently causing issues with quant stats (v0.0.59)
pandas_datareader
pandas_market_calendars>=4.1.2
plotly
flask-socketio
flask-sqlalchemy
flask-marshmallow
marshmallow-sqlalchemy
flask-security
email_validator
bcrypt
pytest
scipy
quantstats==0.0.59
ccxt==3.0.61
termcolor
jsonpickle
apscheduler==3.10.1
15 changes: 15 additions & 0 deletions requirements_dev.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Include the main project requirements
-r requirements.txt

# Unit Test modules
pytest
pytest-mock
coverage

# Linting modules
flake8
isort

# Security checkers
bandit
safety
48 changes: 48 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,51 @@ filterwarnings =

testpaths = tests
norecursedirs = docs .* *.egg* appdir jupyter *pycache* venv* .cache* .coverage* .git data docs docsrc

# .coveragerc to control coverag.py
[coverage:run]
command_line = -m pytest -vv
branch = True
omit =
# * so you can get all dirs
*__init__.py

# Dirs to ignore
tests/*
venv*
.cache/
.git/
.cache-directory
.pytest*
docs/
data/
docsrc/
example*
cache*

[coverage:report]
# Regexs for lines to exclude from consideration
exclude_lines =
# Have to re-enable the standard pragma
pragma: no coverag

# Don't complain about missing debug-only code
def __repr__
if self\.debug-only

# Don't complain if tests don't hit defensive assertion code:
raise AssertionError
raise NotImplementedError

# Don't complain if non-runable code isn't run
if 0:
if __name__ == __main__:

precision = 2
ignore_errors = True
show_missing = True
# Add this back in when coverage is at an acceptable level
# fail_under = 98

[coverage:html]
directory = .coverage.html_report
2 changes: 1 addition & 1 deletion tests/test_alpaca.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import pytest
from pandas import Timestamp

from credentials import ALPACA_CONFIG
# from credentials import ALPACA_CONFIG # Put back in when ready to test
from lumibot.brokers.alpaca import Alpaca
from lumibot.entities.asset import Asset
from lumibot.entities.bars import Bars
Expand Down
2 changes: 1 addition & 1 deletion tests/test_ccxt.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from decimal import Decimal

import pytest
from credentials import BINANCE_CONFIG
# from credentials import BINANCE_CONFIG # Put back in when ready to test

from lumibot.brokers.ccxt import Ccxt
from lumibot.entities.asset import Asset
Expand Down
24 changes: 24 additions & 0 deletions tests/test_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,27 @@ def test_is_option(self):
asset = Asset("SPY", asset_type="option")
order = Order(asset=asset, quantity=10, side="buy", strategy='abc')
assert order.is_option()

def test_get_filled_price(self):
asset = Asset("SPY")
buy_order = Order(strategy='abc', asset=asset, side="buy", quantity=100)
buy_order.transactions = [
Order.Transaction(quantity=50, price=20.0),
Order.Transaction(quantity=50, price=30.0)
]

# Order not yet filled
assert buy_order.get_filled_price() == 0

# Order filled
buy_order.position_filled = True
assert buy_order.get_filled_price() == 25.0

# Ensure Weighted Average used
sell_order = Order(strategy='abc', asset=asset, side="sell", quantity=100)
sell_order.transactions = [
Order.Transaction(quantity=80, price=30.0),
Order.Transaction(quantity=20, price=40.0)
]
sell_order.position_filled = True
assert sell_order.get_filled_price() == 32.0
davidlatte marked this conversation as resolved.
Show resolved Hide resolved
7 changes: 7 additions & 0 deletions tests/test_tradingfee.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from lumibot.entities import TradingFee


class TestTradingFee:
def test_init(self):
fee = TradingFee(flat_fee=5.2)
assert fee.flat_fee == 5.2
davidlatte marked this conversation as resolved.
Show resolved Hide resolved