Skip to content

Commit

Permalink
Use alpha vantage module (#107)
Browse files Browse the repository at this point in the history
* Portfolio fetch stock prices using alpha-vantage python module

* alphavantage call respect the configurable polling period
  • Loading branch information
ilcardella committed Dec 24, 2019
1 parent 373c6e5 commit c7a37b8
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 76 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## []
### Changed
- Replaced TK user interface with GTK+ 3
- Tickers prices are fetched using `alpha-vantage` Python module
- **alpha_vantage_polling_period** configuration parameter is used to wait between each AV call

### Added
- Status bar showing portfolio filepath
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ These are the descriptions of each parameter:
- **trading_logs**: The absolute path of the trading logs to automatically load on startup
- **general/credentials_filepath**: File path of the .credentials file
- **alpha_vantage/api_base_uri**: Base URI of AlphaVantage API
- **alpha_vantage/polling_period_sec**: The polling period to query AlphaVantage for stock prices
- **alpha_vantage/polling_period_sec**: The period of time (in seconds) between each AlphaVantage query for stock prices

## Start TradingMate

Expand Down
2 changes: 1 addition & 1 deletion config/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@
},
"alpha_vantage": {
"api_base_uri": "https://www.alphavantage.co/query",
"polling_period_sec": 30
"polling_period_sec": 12
}
}
113 changes: 113 additions & 0 deletions src/Model/AlphaVantageInterface.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import os
import sys
import inspect
import logging
import traceback
from enum import Enum
import datetime as dt
import time
from alpha_vantage.timeseries import TimeSeries

currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
parentdir = os.path.dirname(currentdir)
sys.path.insert(0, parentdir)

from Utils.Utils import Markets


class AVInterval(Enum):
"""
AlphaVantage interval types: '1min', '5min', '15min', '30min', '60min', 'daily', 'weekly and 'monthly'
"""

MIN_1 = "1min"
MIN_5 = "5min"
MIN_15 = "15min"
MIN_30 = "30min"
MIN_60 = "60min"
DAILY = "daily"
WEEKLY = "weekly"
MONTHLY = "monthly"


class AlphaVantageInterface:
"""class providing interfaces to request data from AlphaVantage"""

# Inner class to create a Singleton pattern
class __AlphaVantageInterface:
def __init__(self, config):
self._config = config
self._last_call_ts = dt.datetime.now()
self._TS = TimeSeries(
key=config.get_alpha_vantage_api_key(),
output_format="json",
treat_info_as_error=True,
)

def daily(self, market_id):
"""
Calls AlphaVantage API and return the Daily time series for the given market
- **market_id**: string representing an AlphaVantage compatible market id
- Returns **None** if an error occurs otherwise the pandas dataframe
"""
self._wait_before_call()
try:
market_id = self._format_market_id(market_id)
data, meta_data = self._TS.get_daily(
symbol=market_id, outputsize="compact"
)
return data
except Exception as e:
logging.error("AlphaVantage wrong api call for {}".format(market_id))
logging.debug(e)
logging.debug(traceback.format_exc())
logging.debug(sys.exc_info()[0])
return None

def _format_market_id(self, market_id):
"""
Convert a standard market id to be compatible with AlphaVantage API.
Adds the market exchange prefix (i.e. London is LON:)
"""
# return "{}:{}".format("LON", market_id.split("-")[0])
if "LSE:" in market_id:
subs = market_id.split(":")
assert len(subs) == 2
return "{}:{}".format(Markets[subs[0]].value, subs[1])
return market_id

def _wait_before_call(self):
"""
Wait between API calls to not overload the server
"""
while (dt.datetime.now() - self._last_call_ts) <= dt.timedelta(
seconds=self._config.get_alpha_vantage_polling_period()
):
time.sleep(0.2)
self._last_call_ts = dt.datetime.now()

# Single instance of the inner class
_instance = None

def __init__(self, config):
if not AlphaVantageInterface._instance:
AlphaVantageInterface._instance = AlphaVantageInterface.__AlphaVantageInterface(
config
)
logging.info("AlphaVantageInterface initialised")

def get_prices(self, market_id, interval=AVInterval.DAILY):
"""
Return the price time series of the requested market with the interval
granularity. Return None if the interval is invalid
"""
if interval == AVInterval.DAILY:
return self._instance.daily(market_id)
else:
logging.warning(
"AlphaVantageInterface supports only DAILY interval. Requested interval: {}".format(
interval.value
)
)
return None
3 changes: 0 additions & 3 deletions src/Model/DatabaseHandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ def __init__(self, config, trading_log_path):
self.db_name = "unknown"
self.trading_history = []
self.read_data(self.db_filepath)
logging.info("DatabaseHandler initialised")

def read_data(self, filepath=None):
"""
Expand Down Expand Up @@ -82,7 +81,6 @@ def add_trade(self, trade):
"""
try:
self.trading_history.append(trade)
logging.info("DatabaseHandler - adding trade {}".format(trade))
except Exception as e:
logging.error(e)
raise RuntimeError("Unable to add trade to the database")
Expand All @@ -93,7 +91,6 @@ def remove_last_trade(self):
"""
try:
del self.trading_history[-1]
logging.info("DatabaseHandler - removed last trade")
except Exception as e:
logging.error(e)
raise RuntimeError("Unable to delete last trade")
23 changes: 15 additions & 8 deletions src/Model/Portfolio.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ def reload(self):
)
for symbol, price in self.price_getter.get_last_data().items():
self._holdings[symbol].set_last_price(price)
logging.info("Portfolio reloaded successfully")
logging.info("Portfolio {} reloaded successfully".format(self._name))
except Exception as e:
logging.error(e)
raise RuntimeError("Unable to reload the portfolio")
Expand Down Expand Up @@ -259,21 +259,27 @@ def is_trade_valid(self, newTrade):
"""
if newTrade.action == Actions.WITHDRAW or newTrade.action == Actions.FEE:
if newTrade.quantity > self.get_cash_available():
logging.warning(Messages.INSUF_FUNDING.value)
logging.warning(
"Portfolio {}: {}".format(self._name, Messages.INSUF_FUNDING.value)
)
raise RuntimeError(Messages.INSUF_FUNDING.value)
elif newTrade.action == Actions.BUY:
cost = (newTrade.price * newTrade.quantity) / 100 # in £
fee = newTrade.fee
tax = (newTrade.sdr * cost) / 100
totalCost = cost + fee + tax
if totalCost > self.get_cash_available():
logging.warning(Messages.INSUF_FUNDING.value)
logging.warning(
"Portfolio {}: {}".format(self._name, Messages.INSUF_FUNDING.value)
)
raise RuntimeError(Messages.INSUF_FUNDING.value)
elif newTrade.action == Actions.SELL:
if newTrade.quantity > self.get_holding_quantity(newTrade.symbol):
logging.warning(Messages.INSUF_HOLDINGS.value)
logging.warning(
"Portfolio {}: {}".format(self._name, Messages.INSUF_HOLDINGS.value)
)
raise RuntimeError(Messages.INSUF_HOLDINGS.value)
logging.info("Portfolio - trade validated")
logging.info("Portfolio {}: new trade validated".format(self._name))
return True

def get_trade_history(self):
Expand Down Expand Up @@ -302,21 +308,22 @@ def save_portfolio(self, filepath):
# PRICE GETTER WORK THREAD

def on_new_price_data(self):
logging.info("Portfolio - new live price available")
priceDict = self.price_getter.get_last_data()
for symbol, price in priceDict.items():
if symbol in self._holdings:
self._holdings[symbol].set_last_price(price)

def on_manual_refresh_live_data(self):
logging.info("Portfolio - manual refresh live price")
logging.info("Portfolio {}: manual refresh of data".format(self._name))
if self.price_getter.is_enabled():
self.price_getter.cancel_timeout()
else:
self.price_getter.force_single_run()

def set_auto_refresh(self, enabled):
logging.info("Portfolio - live price auto refresh: {}".format(enabled))
logging.info(
"Portfolio {}: price auto refresh set to {}".format(self._name, enabled)
)
self.price_getter.enable(enabled)

def get_auto_refresh_enabled(self):
Expand Down
75 changes: 12 additions & 63 deletions src/Model/StockPriceGetter.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import os
import sys
import inspect
import requests
import json
import logging

currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
Expand All @@ -11,96 +9,47 @@

from Utils.TaskThread import TaskThread
from Utils.ConfigurationManager import ConfigurationManager
from Utils.Utils import Markets
from .AlphaVantageInterface import AlphaVantageInterface


class StockPriceGetter(TaskThread):
def __init__(self, config, onNewPriceDataCallback):
TaskThread.__init__(self)
self.config = config
self.onNewPriceDataCallback = onNewPriceDataCallback
def __init__(self, config, update_callback):
super(StockPriceGetter, self).__init__()
self._config = config
self._price_update_callback = update_callback
self.reset()
logging.info("StockPriceGetter initialised")

def _read_configuration(self):
# Override the parent class default value
self._interval = self.config.get_alpha_vantage_polling_period()
self._av = AlphaVantageInterface(config)

def task(self):
priceDict = {}
for symbol in self.symbolList:
if not self._finished.isSet():
value = self._fetch_price_data(symbol)
# Wait as suggested by AlphaVantage support
self._timeout.wait(2)
if value is not None:
priceDict[symbol] = value
if not self._finished.isSet():
self.lastData = priceDict # Store internally
self.onNewPriceDataCallback() # Notify the model
self._price_update_callback() # Notify the model

def _fetch_price_data(self, symbol):
# TODO use alpha_vantage lib instead of manual request
try:
url = self._build_url(
"TIME_SERIES_DAILY",
symbol,
"5min",
self.config.get_alpha_vantage_api_key(),
)
except Exception as e:
logging.error(e)
logging.error(
"StockPriceGetter - Unable to build url for {}".format(symbol)
)
return None
try:
response = requests.get(url)
if response.status_code != 200:
logging.error(
"StockPriceGetter - Request for {} returned code {}".format(
url.split("apikey")[0], response.status_code
)
)
return None
data = json.loads(response.text)
timeSerie = data["Time Series (Daily)"]
last = next(iter(timeSerie.values()))
data = self._av.get_prices(symbol)
assert data is not None
last = next(iter(data.values()))
value = float(last["4. close"])
except Exception:
except Exception as e:
logging.error(
"StockPriceGetter - Unable to fetch data from {}".format(
url.split("apikey")[0]
)
"StockPriceGetter - Unable to fetch data for {}: {}".format(symbol, e)
)
value = None
return value

def _build_url(self, aLength, aSymbol, anInterval, anApiKey):
function = "function={}".format(aLength)
symbol = "symbol={}".format(self.convert_market_to_alphavantage(aSymbol))
apiKey = "apikey={}".format(anApiKey)
return "{}?{}&{}&{}".format(
self.config.get_alpha_vantage_base_url(), function, symbol, apiKey
)

def convert_market_to_alphavantage(self, symbol):
"""
Convert the market (LSE, etc.) into the alphavantage market compatible string
i.e.: the LSE needs to be converted to LON
"""
# Extract the market part from the symbol string
market = str(symbol).split(":")[0]
av_market = Markets[market]
return "{}:{}".format(av_market.value, str(symbol).split(":")[1])

def get_last_data(self):
return self.lastData

def set_symbol_list(self, aList):
self.symbolList = aList

def reset(self):
self._read_configuration()
self.lastData = {}
self.symbolList = []

0 comments on commit c7a37b8

Please sign in to comment.