Skip to content

Commit b3776c0

Browse files
committed
Add new features
1 parent 6351e67 commit b3776c0

File tree

7 files changed

+397
-12
lines changed

7 files changed

+397
-12
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -131,3 +131,4 @@ dmypy.json
131131
pyrobot/config.py
132132
tests/config.py
133133
.vscode/settings.json
134+
indicators/

pyrobot/indicators.py

+206-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,210 @@
1+
import numpy as np
2+
import pandas as pd
3+
14
from typing import Tuple
25
from typing import List
36
from typing import Iterable
47

5-
class Indicator():
6-
pass
8+
from pyrobot.stock_frame import StockFrame
9+
10+
11+
class Indicators():
12+
13+
14+
def __init__(self, price_data_frame: StockFrame) -> None:
15+
"""Initalizes the Indicator Client.
16+
17+
Arguments:
18+
----
19+
price_data_frame {pyrobot.StockFrame} -- The price data frame which is used to add indicators to.
20+
At a minimum this data frame must have the following columns: `['timestamp','close','open','high','low']`.
21+
22+
Usage:
23+
----
24+
>>> historical_prices_df = trading_robot.grab_historical_prices(
25+
start=start_date,
26+
end=end_date,
27+
bar_size=1,
28+
bar_type='minute'
29+
)
30+
>>> price_data_frame = pd.DataFrame(data=historical_prices)
31+
>>> indicator_client = Indicators(price_data_frame=price_data_frame)
32+
>>> price_data_frame_indicators = indicator_client.price_data_frame
33+
"""
34+
35+
self._price_data_frame: StockFrame = price_data_frame
36+
self._price_groups = self._price_data_frame.symbol_groups
37+
38+
if self.is_multi_index:
39+
True
40+
41+
@property
42+
def price_data_frame(self) -> pd.DataFrame:
43+
return self._price_data_frame.frame
44+
45+
@price_data_frame.setter
46+
def price_data_frame(self, price_data_frame):
47+
self._price_data_frame = price_data_frame
48+
49+
@price_data_frame.deleter
50+
def price_data_frame(self):
51+
del self._price_data_frame
52+
53+
@property
54+
def is_multi_index(self):
55+
if isinstance(self._price_data_frame.frame.index, pd.MultiIndex):
56+
return True
57+
else:
58+
return False
59+
60+
def change_in_price(self) -> pd.DataFrame:
61+
"""Calculates the Change in Price.
62+
63+
Returns:
64+
----
65+
pd.DataFrame -- A data frame with the Change in Price.
66+
"""
67+
68+
self._price_data_frame.frame['change_in_price'] = self._price_data_frame.frame.groupby(
69+
by='symbol',
70+
as_index=False
71+
)['close'].transform(
72+
lambda x: x.diff()
73+
)
74+
75+
def rsi(self, period: int, method: str = 'wilders') -> pd.DataFrame:
76+
"""Calculates the Relative Strength Index (RSI).
77+
78+
Arguments:
79+
----
80+
period {int} -- The number of periods to use to calculate the RSI.
81+
82+
Keyword Arguments:
83+
----
84+
method {str} -- The calculation methodology. (default: {'wilders'})
85+
86+
Returns:
87+
----
88+
pd.DataFrame -- A Pandas data frame with the RSI indicator included.
89+
90+
Usage:
91+
----
92+
>>> historical_prices_df = trading_robot.grab_historical_prices(
93+
start=start_date,
94+
end=end_date,
95+
bar_size=1,
96+
bar_type='minute'
97+
)
98+
>>> price_data_frame = pd.DataFrame(data=historical_prices)
99+
>>> indicator_client = Indicators(price_data_frame=price_data_frame)
100+
>>> indicator_client.rsi(period=14)
101+
>>> price_data_frame = inidcator_client.price_data_frame
102+
"""
103+
104+
# Define the price data frame.
105+
price_frame = self._price_data_frame.frame
106+
107+
# First calculate the Change in Price.
108+
if 'change_in_price' not in price_frame.columns:
109+
self.change_in_price()
110+
111+
# Define the up days.
112+
price_frame['up_day'] = price_frame.groupby(
113+
by='symbol',
114+
as_index=False
115+
)['change_in_price'].transform(lambda x : np.where(x >= 0, x, 0))
116+
117+
# Define the down days.
118+
price_frame['down_day'] = price_frame.groupby(
119+
by='symbol',
120+
as_index=False
121+
)['change_in_price'].transform(lambda x : np.where(x < 0, x.abs(), 0))
122+
123+
# Calculate the EWMA (Exponential Weighted Moving Average), meaning older values are given less weight compared to newer values.
124+
price_frame['ewma_up'] = price_frame.groupby('symbol')['up_day'].transform(lambda x: x.ewm(span = period).mean())
125+
price_frame['ewma_down'] = price_frame.groupby('symbol')['down_day'].transform(lambda x: x.ewm(span = period).mean())
126+
127+
# Calculate the Relative Strength
128+
relative_strength = price_frame['ewma_up'] / price_frame['ewma_down']
129+
130+
# Calculate the Relative Strength Index
131+
relative_strength_index = 100.0 - (100.0 / (1.0 + relative_strength))
132+
133+
# Add the info to the data frame.
134+
price_frame['rsi'] = np.where(relative_strength_index == 0, 100, 100 - (100 / (1 + relative_strength_index)))
135+
136+
# Clean up before sending back.
137+
price_frame.drop(labels=['ewma_up', 'ewma_down', 'down_day', 'up_day', 'change_in_price'], axis=1, inplace=True)
138+
139+
# Reassign?
140+
# self.price_data_frame(price_frame)
141+
142+
return price_frame
143+
144+
def sma(self, period: int) -> pd.DataFrame:
145+
"""Calculates the Simple Moving Average (SMA).
146+
147+
Arguments:
148+
----
149+
period {int} -- The number of periods to use when calculating the SMA.
150+
151+
Returns:
152+
----
153+
pd.DataFrame -- A Pandas data frame with the SMA indicator included.
154+
155+
Usage:
156+
----
157+
>>> historical_prices_df = trading_robot.grab_historical_prices(
158+
start=start_date,
159+
end=end_date,
160+
bar_size=1,
161+
bar_type='minute'
162+
)
163+
>>> price_data_frame = pd.DataFrame(data=historical_prices)
164+
>>> indicator_client = Indicators(price_data_frame=price_data_frame)
165+
>>> indicator_client.sma(period=100)
166+
>>> price_data_frame = inidcator_client.price_data_frame
167+
"""
168+
169+
# Grab the Price Frame.
170+
price_frame = self._price_data_frame.frame
171+
172+
# Add the SMA
173+
price_frame['sma'] = price_frame.groupby('symbol')['close'].transform(lambda x: x.rolling(window=period).mean())
174+
175+
return price_frame
176+
177+
def ema(self, period: int, alpha: float = 0.0) -> pd.DataFrame:
178+
"""Calculates the Exponential Moving Average (EMA).
179+
180+
Arguments:
181+
----
182+
period {int} -- The number of periods to use when calculating the EMA.
183+
184+
Returns:
185+
----
186+
pd.DataFrame -- A Pandas data frame with the EMA indicator included.
187+
188+
Usage:
189+
----
190+
>>> historical_prices_df = trading_robot.grab_historical_prices(
191+
start=start_date,
192+
end=end_date,
193+
bar_size=1,
194+
bar_type='minute'
195+
)
196+
>>> price_data_frame = pd.DataFrame(data=historical_prices)
197+
>>> indicator_client = Indicators(price_data_frame=price_data_frame)
198+
>>> indicator_client.ema(period=50)
199+
>>> price_data_frame = inidcator_client.price_data_frame
200+
"""
201+
202+
# Grab the Price Frame.
203+
price_frame = self._price_data_frame.frame
204+
205+
# Add the EMA
206+
price_frame['ema'] = price_frame.groupby('symbol', sort=True)['close'].transform(
207+
lambda x: x.ewm(span=period).mean()
208+
)
209+
210+
return price_frame

pyrobot/portfolio.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ def __init__(self, account_number: str = None) -> None:
88
"""Initalizes a new instance of the Portfolio object.
99
1010
Keyword Arguments:
11-
account_number {str} -- An accout number to associate with the Portfolio. (default: {None})
11+
----
12+
account_number {str} -- An accout number to associate with the Portfolio. (default: {None})
1213
"""
1314

1415
self.positions: dict = {}
@@ -93,8 +94,7 @@ def add_positions(self, positions: List[dict]) -> dict:
9394
else:
9495
raise TypeError('Positions must be a list of dictionaries.')
9596

96-
def add_position(self, symbol: str, asset_type: str, quantity: int = 0, purchase_price: float = 0.00,
97-
purchase_date: str = None) -> dict:
97+
def add_position(self, symbol: str, asset_type: str, quantity: int = 0, purchase_price: float = 0.00, purchase_date: str = None) -> dict:
9898
"""Adds a single new position to the the portfolio.
9999
100100
Arguments:
@@ -220,7 +220,7 @@ def in_portfolio(self, symbol: str) -> bool:
220220
221221
Returns:
222222
----
223-
bool -- `True` if the position is in the portfolio, `False` otherwise.
223+
bool -- `True` if the position is in the portfolio, `False` otherwise.
224224
"""
225225

226226
if symbol in self.positions:
@@ -243,8 +243,8 @@ def is_porfitable(self, symbol: str, current_price: float) -> bool:
243243
244244
Returns:
245245
----
246-
bool -- Specifies whether the position is profitable or flat (True) or not
247-
profitable (False).
246+
bool -- Specifies whether the position is profitable or flat (True) or not
247+
profitable (False).
248248
"""
249249

250250
if symbol in self.positions and self.positions[symbol]['purchase_price'] < current_price:

pyrobot/robot.py

+89-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,19 @@
1+
import pandas as pd
2+
13
from td.client import TDClient
2-
from datetime import datetime, time, timezone
4+
from td.utils import milliseconds_since_epoch
5+
6+
from datetime import datetime
7+
from datetime import time
8+
from datetime import timezone
9+
310
from pyrobot.portfolio import Portfolio
411
from pyrobot.trades import Trade
12+
from pyrobot.stock_frame import StockFrame
13+
14+
from typing import List
15+
from typing import Dict
16+
from typing import Union
517

618
class PyRobot():
719

@@ -29,9 +41,11 @@ def __init__(self, client_id: str, redirect_uri: str, credentials_path: str = No
2941
self.trading_account: str = trading_account
3042
self.client_id: str = client_id
3143
self.redirect_uri: str = redirect_uri
32-
self.credentials_path:str = credentials_path
44+
self.credentials_path: str = credentials_path
3345
self.session: TDClient = self._create_session()
3446
self.trades: dict = {}
47+
self.historical_prices: dict = {}
48+
self.stock_frame = None
3549

3650
def _create_session(self) -> TDClient:
3751
"""Start a new session.
@@ -351,3 +365,76 @@ def grab_current_quotes(self) -> dict:
351365
quotes = self.session.get_quotes(instruments = list(symbols))
352366

353367
return quotes
368+
369+
def grab_historical_prices(self, start: datetime, end: datetime, bar_size: int = 1, bar_type: str = 'minute', data_frame: bool = False) -> Union[List[Dict], pd.DataFrame]:
370+
"""Grabs the historical prices for all the postions in a portfolio.
371+
372+
Overview:
373+
----
374+
Any of the historical price data returned will include extended hours
375+
price data by default.
376+
377+
Arguments:
378+
----
379+
start {datetime} -- Defines the start date for the historical prices.
380+
381+
end {datetime} -- Defines the end date for the historical prices.
382+
383+
Keyword Arguments:
384+
----
385+
bar_size {int} -- Defines the size of each bar. (default: {1})
386+
387+
bar_type {str} -- Defines the bar type, can be one of the following:
388+
`['minute', 'week', 'month', 'year']` (default: {'minute'})
389+
390+
Returns:
391+
----
392+
{List[Dict]} -- The historical price candles.
393+
394+
Usage:
395+
----
396+
"""
397+
398+
start = str(milliseconds_since_epoch(dt_object=start))
399+
end = str(milliseconds_since_epoch(dt_object=end))
400+
401+
new_prices = []
402+
403+
for symbol in self.portfolio.positions:
404+
405+
historical_prices_response = self.session.get_price_history(
406+
symbol=symbol,
407+
period_type='day',
408+
start_date=start,
409+
end_date=end,
410+
frequency_type=bar_type,
411+
frequency=bar_size,
412+
extended_hours=True
413+
)
414+
415+
self.historical_prices[symbol] = {}
416+
self.historical_prices[symbol]['candles'] = historical_prices_response['candles']
417+
418+
for candle in historical_prices_response['candles']:
419+
420+
new_price_mini_dict = {}
421+
new_price_mini_dict['symbol'] = symbol
422+
new_price_mini_dict['open'] = candle['open']
423+
new_price_mini_dict['close'] = candle['close']
424+
new_price_mini_dict['high'] = candle['high']
425+
new_price_mini_dict['low'] = candle['low']
426+
new_price_mini_dict['volume'] = candle['volume']
427+
new_price_mini_dict['datetime'] = candle['datetime']
428+
new_prices.append(new_price_mini_dict)
429+
430+
self.historical_prices['aggregated'] = new_prices
431+
432+
return self.historical_prices
433+
434+
def create_stock_frame(self, data: List[dict]) -> StockFrame:
435+
436+
self.stock_frame = StockFrame(data=data)
437+
438+
return self.stock_frame
439+
440+

0 commit comments

Comments
 (0)