Merge branch 'develop'

This commit is contained in:
fredfortier
2017-11-02 20:46:06 -04:00
45 changed files with 18451 additions and 888 deletions
+1 -1
View File
@@ -9,7 +9,7 @@ from six import text_type
from catalyst.data import bundles as bundles_module
from catalyst.exchange.exchange_bundle import ExchangeBundle
from catalyst.exchange.init_utils import get_exchange
from catalyst.exchange.factory import get_exchange
from catalyst.utils.cli import Date, Timestamp
from catalyst.utils.run_algo import _run, load_extensions
+14
View File
@@ -559,6 +559,20 @@ cdef class TradingPair(Asset):
end_minute=self.end_minute
)
def is_exchange_open(self, dt_minute):
"""
Parameters
----------
dt_minute: pd.Timestamp (UTC, tz-aware)
The minute to check.
Returns
-------
boolean: whether the asset's exchange is open at the given minute.
"""
#TODO: consider implementing to spot holds
return True
cpdef __reduce__(self):
"""
Function used by pickle to determine how to serialize/deserialize this
+28 -26
View File
@@ -1,13 +1,13 @@
import talib
import pandas as pd
from catalyst import run_algorithm
from catalyst.api import symbol
def initialize(context):
print('initializing')
context.asset = symbol('burst_btc')
context.asset = symbol('eth_btc')
def handle_data(context, data):
@@ -16,36 +16,38 @@ def handle_data(context, data):
price = data.current(context.asset, 'close')
print('got price {price}'.format(price=price))
prices = data.history(
context.asset,
fields='price',
bar_count=15,
frequency='1d'
)
rsi = talib.RSI(prices.values, timeperiod=14)[-1]
print('got rsi: {}'.format(rsi))
pass
try:
prices = data.history(
context.asset,
fields='price',
bar_count=16,
frequency='5T'
)
rsi = talib.RSI(prices.values, timeperiod=14)[-1]
print('got rsi: {}'.format(rsi))
except Exception as e:
print(e)
run_algorithm(
capital_base=250,
start=pd.to_datetime('2017-08-01', utc=True),
end=pd.to_datetime('2017-9-30', utc=True),
data_frequency='minute',
initialize=initialize,
handle_data=handle_data,
analyze=None,
exchange_name='poloniex',
algo_namespace='simple_loop',
base_currency='btc'
)
# run_algorithm(
# capital_base=250,
# start=pd.to_datetime('2017-1-1', utc=True),
# end=pd.to_datetime('2017-10-22', utc=True),
# data_frequency='daily',
# initialize=initialize,
# handle_data=handle_data,
# analyze=None,
# exchange_name='bitfinex',
# live=True,
# algo_namespace='simple_loop',
# base_currency='eth',
# live_graph=False
# base_currency='btc'
# )
run_algorithm(
initialize=initialize,
handle_data=handle_data,
analyze=None,
exchange_name='poloniex',
live=True,
algo_namespace='simple_loop',
base_currency='eth',
live_graph=False
)
+15 -18
View File
@@ -240,7 +240,7 @@ class Bitfinex(Exchange):
# TODO: fetch account data and keep in cache
return None
def get_candles(self, data_frequency, assets, bar_count=None,
def get_candles(self, freq, assets, bar_count=None,
start_dt=None, end_dt=None):
"""
Retrieve OHLVC candles from Bitfinex
@@ -259,39 +259,36 @@ class Bitfinex(Exchange):
'retrieving {bars} {freq} candles on {exchange} from '
'{end_dt} for markets {symbols}, '.format(
bars=bar_count,
freq=data_frequency,
freq=freq,
exchange=self.name,
end_dt=end_dt,
symbols=get_symbols_string(assets)
)
)
freq_match = re.match(r'([0-9].*)(m|h|d)', data_frequency, re.M | re.I)
allowed_frequencies = ['1T', '5T', '15T', '30T', '60T', '180T',
'360T', '720T', '1D', '7D', '14D', '30D']
if freq not in allowed_frequencies:
raise InvalidHistoryFrequencyError(frequency=freq)
freq_match = re.match(r'([0-9].*)(T|H|D)', freq, re.M | re.I)
if freq_match:
number = int(freq_match.group(1))
unit = freq_match.group(2)
if unit == 'd':
converted_unit = 'D'
if unit == 'T':
if number in [60, 180, 360, 720]:
number = number / 60
converted_unit = 'h'
else:
converted_unit = 'm'
else:
converted_unit = unit
frequency = '{}{}'.format(number, converted_unit)
allowed_frequencies = ['1m', '5m', '15m', '30m', '1h', '3h', '6h',
'12h', '1D', '7D', '14D', '1M']
if frequency not in allowed_frequencies:
raise InvalidHistoryFrequencyError(
frequency=data_frequency
)
elif data_frequency == 'minute':
frequency = '1m'
elif data_frequency == 'daily':
frequency = '1D'
else:
raise InvalidHistoryFrequencyError(
frequency=data_frequency
)
raise InvalidHistoryFrequencyError(frequency=freq)
# Making sure that assets are iterable
asset_list = [assets] if isinstance(assets, TradingPair) else assets
+10 -12
View File
@@ -210,14 +210,14 @@ class Bittrex(Exchange):
error=status['message']
)
def get_candles(self, data_frequency, assets, bar_count=None,
def get_candles(self, freq, assets, bar_count=None,
start_dt=None, end_dt=None):
"""
Supported Intervals
-------------------
day, oneMin, fiveMin, thirtyMin, hour
:param data_frequency:
:param freq:
:param assets:
:param bar_count:
:param start_dt
@@ -233,28 +233,25 @@ class Bittrex(Exchange):
'retrieving {bars} {freq} candles on {exchange} from '
'{end_dt} for markets {symbols}, '.format(
bars=bar_count,
freq=data_frequency,
freq=freq,
exchange=self.name,
end_dt=end_dt,
symbols=get_symbols_string(assets)
)
)
data_frequency = data_frequency.lower()
if data_frequency == 'minute' or data_frequency == '1m':
if freq == '1T':
frequency = 'oneMin'
elif data_frequency == '5m':
elif freq == '5T':
frequency = 'fiveMin'
elif data_frequency == '30m':
elif freq == '30T':
frequency = 'thirtyMin'
elif data_frequency == '1h':
elif freq == '60T':
frequency = 'hour'
elif data_frequency == 'daily' or data_frequency == '1d':
elif freq == '1D':
frequency = 'day'
else:
raise InvalidHistoryFrequencyError(
frequency=data_frequency
)
raise InvalidHistoryFrequencyError(frequency=freq)
# Making sure that assets are iterable
asset_list = [assets] if isinstance(assets, TradingPair) else assets
@@ -297,6 +294,7 @@ class Bittrex(Exchange):
if bar_count is None:
ohlc_map[asset] = ohlc_from_candle(ordered_candles[0])
else:
# TODO: optimize
ohlc_bars = []
for candle in ordered_candles[:bar_count]:
ohlc = ohlc_from_candle(candle)
+178 -79
View File
@@ -7,22 +7,42 @@ import numpy as np
import pandas as pd
import pytz
from catalyst.data.bundles import from_bundle_ingest_dirname
from catalyst.data.bundles.core import download_without_progress
from catalyst.exchange.exchange_errors import NoDataAvailableOnExchange
from catalyst.exchange.exchange_utils import get_exchange_bundles_folder
from catalyst.utils.deprecate import deprecated
from catalyst.utils.paths import data_path
EXCHANGE_NAMES = ['bitfinex', 'bittrex', 'poloniex']
API_URL = 'http://data.enigma.co/api/v1'
def get_date_from_ms(ms):
"""
The date from the number of miliseconds from the epoch.
Parameters
----------
ms: int
Returns
-------
datetime
"""
return datetime.fromtimestamp(ms / 1000.0)
def get_seconds_from_date(date):
"""
The number of seconds from the epoch.
Parameters
----------
date: datetime
Returns
-------
int
"""
epoch = datetime.utcfromtimestamp(0)
epoch = epoch.replace(tzinfo=pytz.UTC)
@@ -33,16 +53,19 @@ def get_bcolz_chunk(exchange_name, symbol, data_frequency, period):
"""
Download and extract a bcolz bundle.
:param exchange_name:
:param symbol:
:param data_frequency:
:param period:
:return:
Parameters
----------
exchange_name: str
symbol: str
data_frequency: str
period: str
Note:
Returns
-------
str
Filename: bitfinex-daily-neo_eth-2017-10.tar.gz
"""
"""
root = get_exchange_bundles_folder(exchange_name)
name = '{exchange}-{frequency}-{symbol}-{period}'.format(
exchange=exchange_name,
@@ -67,32 +90,80 @@ def get_bcolz_chunk(exchange_name, symbol, data_frequency, period):
def get_delta(periods, data_frequency):
"""
Get a time delta based on the specified data frequency.
Parameters
----------
periods: int
data_frequency: str
Returns
-------
timedelta
"""
return timedelta(minutes=periods) \
if data_frequency == 'minute' else timedelta(days=periods)
def get_periods_range(start_dt, end_dt, data_frequency):
freq = 'T' if data_frequency == 'minute' else 'D'
def get_periods_range(start_dt, end_dt, freq):
"""
Get a date range for the specified parameters.
Parameters
----------
start_dt: datetime
end_dt: datetime
freq: str
Returns
-------
DateTimeIndex
"""
if freq == 'minute':
freq = 'T'
elif freq == 'daily':
freq = 'D'
return pd.date_range(start_dt, end_dt, freq=freq)
def get_periods(start_dt, end_dt, data_frequency):
delta = end_dt - start_dt
def get_periods(start_dt, end_dt, freq):
"""
The number of periods in the specified range.
if data_frequency == 'minute':
delta_periods = delta.total_seconds() / 60
Parameters
----------
start_dt: datetime
end_dt: datetime
freq: str
elif data_frequency == 'daily':
delta_periods = delta.total_seconds() / 60 / 60 / 24
Returns
-------
int
else:
raise ValueError('frequency not supported')
return int(delta_periods)
"""
return len(get_periods_range(start_dt, end_dt, freq))
def get_start_dt(end_dt, bar_count, data_frequency):
"""
The start date based on specified end date and data frequency.
Parameters
----------
end_dt: datetime
bar_count: int
data_frequency: str
Returns
-------
datetime
"""
periods = bar_count
if periods > 1:
delta = get_delta(periods, data_frequency)
@@ -103,41 +174,96 @@ def get_start_dt(end_dt, bar_count, data_frequency):
return start_dt
def get_month_start_end(dt):
def get_period_label(dt, data_frequency):
"""
Returns the first and last day of the month for the specified date.
The period label for the specified date and frequency.
Parameters
----------
dt: datetime
data_frequency: str
Returns
-------
str
"""
return '{}-{:02d}'.format(dt.year, dt.month) if data_frequency == 'minute' \
else '{}'.format(dt.year)
def get_month_start_end(dt, first_day=None, last_day=None):
"""
The first and last day of the month for the specified date.
Parameters
----------
dt: datetime
first_day: datetime
last_day: datetime
Returns
-------
datetime, datetime
:param dt:
:return:
"""
month_range = calendar.monthrange(dt.year, dt.month)
month_start = pd.to_datetime(datetime(
dt.year, dt.month, 1, 0, 0, 0, 0
), utc=True)
month_end = pd.to_datetime(datetime(
dt.year, dt.month, month_range[1], 23, 59, 0, 0
), utc=True)
if first_day:
month_start = first_day
else:
month_start = pd.to_datetime(datetime(
dt.year, dt.month, 1, 0, 0, 0, 0
), utc=True)
if last_day:
month_end = last_day
else:
month_end = pd.to_datetime(datetime(
dt.year, dt.month, month_range[1], 23, 59, 0, 0
), utc=True)
return month_start, month_end
def get_year_start_end(dt):
def get_year_start_end(dt, first_day=None, last_day=None):
"""
Returns the first and last day of the year for the specified date.
The first and last day of the year for the specified date.
Parameters
----------
dt: datetime
first_day: datetime
last_day: datetime
Returns
-------
datetime, datetime
:param dt:
:return:
"""
year_start = pd.to_datetime(date(dt.year, 1, 1), utc=True)
year_end = pd.to_datetime(date(dt.year, 12, 31), utc=True)
year_start = first_day if first_day \
else pd.to_datetime(date(dt.year, 1, 1), utc=True)
year_end = last_day if last_day \
else pd.to_datetime(date(dt.year, 12, 31), utc=True)
return year_start, year_end
def get_df_from_arrays(arrays, periods):
"""
A DataFrame from the specified OHCLV arrays.
Parameters
----------
arrays: Object
periods: DateTimeIndex
Returns
-------
DataFrame
"""
ohlcv = dict()
for index, field in enumerate(
['open', 'high', 'low', 'close', 'volume']):
@@ -155,11 +281,17 @@ def range_in_bundle(asset, start_dt, end_dt, reader):
Evaluate whether price data of an asset is included has been ingested in
the exchange bundle for the given date range.
:param asset:
:param start_dt:
:param end_dt:
:param reader:
:return:
Parameters
----------
asset: TradingPair
start_dt: datetime
end_dt: datetime
reader: BcolzBarMinuteReader
Returns
-------
bool
"""
has_data = True
if has_data and reader is not None:
@@ -183,36 +315,3 @@ def range_in_bundle(asset, start_dt, end_dt, reader):
has_data = False
return has_data
@deprecated
def find_most_recent_time(bundle_name):
"""
Find most recent "time folder" for a given bundle.
:param bundle_name:
The name of the targeted bundle.
:return folder:
The name of the time folder.
"""
try:
bundle_folders = os.listdir(
data_path([bundle_name]),
)
except OSError:
return None
most_recent_bundle = dict()
for folder in bundle_folders:
date = from_bundle_ingest_dirname(folder)
if not most_recent_bundle or date > \
most_recent_bundle[list(most_recent_bundle.keys())[0]]:
most_recent_bundle = dict()
most_recent_bundle[folder] = date
if most_recent_bundle:
return list(most_recent_bundle.keys())[0]
else:
return None
+260 -113
View File
@@ -1,5 +1,4 @@
import abc
import re
from abc import ABCMeta, abstractmethod, abstractproperty
from datetime import timedelta
from time import sleep
@@ -12,17 +11,20 @@ from logbook import Logger
from catalyst.constants import LOG_LEVEL
from catalyst.data.data_portal import BASE_FIELDS
from catalyst.exchange.bundle_utils import get_start_dt, \
get_delta, get_periods
get_delta, get_periods, get_periods_range
from catalyst.exchange.exchange_bundle import ExchangeBundle
from catalyst.exchange.exchange_errors import MismatchingBaseCurrencies, \
InvalidOrderStyle, BaseCurrencyNotFoundError, SymbolNotFoundOnExchange, \
InvalidHistoryFrequencyError, PricingDataNotLoadedError
PricingDataNotLoadedError, \
NoDataAvailableOnExchange
from catalyst.exchange.exchange_execution import ExchangeStopLimitOrder, \
ExchangeLimitOrder, ExchangeStopOrder
from catalyst.exchange.exchange_portfolio import ExchangePortfolio
from catalyst.exchange.exchange_utils import get_exchange_symbols
from catalyst.exchange.exchange_utils import get_exchange_symbols, \
get_frequency, resample_history_df
from catalyst.finance.order import ORDER_STATUS
from catalyst.finance.transaction import Transaction
from catalyst.utils.deprecate import deprecated
log = Logger('Exchange', level=LOG_LEVEL)
@@ -50,9 +52,11 @@ class Exchange:
@property
def portfolio(self):
"""
Return the Portfolio
The exchange portfolio
:return:
Returns
-------
ExchangePortfolio
"""
if self._portfolio is None:
self._portfolio = ExchangePortfolio(
@@ -70,6 +74,22 @@ class Exchange:
def time_skew(self):
pass
def is_open(self, dt):
"""
Is the exchange open
Parameters
----------
dt: Timestamp
Returns
-------
bool
"""
# TODO: implement for each exchange.
return True
def ask_request(self):
"""
Asks permission to issue a request to the exchange.
@@ -78,7 +98,9 @@ class Exchange:
The application will pause if the maximum requests per minute
permitted by the exchange is exceeded.
:return boolean:
Returns
-------
bool
"""
now = pd.Timestamp.utcnow()
@@ -110,10 +132,16 @@ class Exchange:
def get_symbol(self, asset):
"""
Get the exchange specific symbol of the given asset.
The the exchange specific symbol of the specified market.
Parameters
----------
asset: TradingPair
Returns
-------
str
:param asset: Asset
:return: symbol: str
"""
symbol = None
@@ -131,17 +159,34 @@ class Exchange:
"""
Get a list of symbols corresponding to each given asset.
:param assets: Asset[]
:return:
Parameters
----------
assets: list[TradingPair]
Returns
-------
list[str]
"""
symbols = []
for asset in assets:
symbols.append(self.get_symbol(asset))
return symbols
def get_assets(self, symbols=None):
"""
The list of markets for the specified symbols.
Parameters
----------
symbols: list[str]
Returns
-------
list[TradingPair]
"""
assets = []
if symbols is not None:
@@ -156,9 +201,16 @@ class Exchange:
def get_asset(self, symbol):
"""
Find an Asset on the current exchange based on its Catalyst symbol
:param symbol: the [target]_[base] currency pair symbol
:return: Asset
The market for the specified symbol.
Parameters
----------
symbol: str
Returns
-------
TradingPair
"""
asset = None
@@ -189,7 +241,6 @@ class Exchange:
currency pair symbol. The universal symbol is contained in the
'symbol' attribute of each asset.
Notes
-----
The sid of each asset is calculated based on a numeric hash of the
@@ -198,8 +249,8 @@ class Exchange:
This method can be overridden if an exchange offers equivalent data
via its api.
"""
"""
symbol_map = self.fetch_symbol_map()
for exchange_symbol in symbol_map:
asset = symbol_map[exchange_symbol]
@@ -260,8 +311,10 @@ class Exchange:
For each executed order found, create a transaction and apply to the
Portfolio.
:return:
transactions: Transaction[]
Returns
-------
list[Transaction]
"""
transactions = list()
if self.portfolio.open_orders:
@@ -344,17 +397,24 @@ class Exchange:
"""
Similar to 'get_spot_value' but for a single asset
Note
----
Notes
-----
We're writing each minute bar to disk using zipline's machinery.
This is especially useful when running multiple algorithms
concurrently. By using local data when possible, we try to reaching
request limits on exchanges.
:param asset:
:param field:
:param data_frequency:
:return value: The spot value of the given asset / field
Parameters
----------
asset: TradingPair
field: str
data_frequency: str
Returns
-------
float
The spot value of the given asset / field
"""
log.debug(
'fetching spot value {field} for symbol {symbol}'.format(
@@ -363,7 +423,8 @@ class Exchange:
)
)
ohlc = self.get_candles(data_frequency, asset)
freq = '1T' if data_frequency == 'minute' else '1D'
ohlc = self.get_candles(freq, asset)
if field not in ohlc:
raise KeyError('Invalid column: %s' % field)
@@ -377,35 +438,45 @@ class Exchange:
"""
Get a series of field data for the specified candles.
:param candles:
:param start_dt:
:param end_dt:
:param field:
:param previous_value:
:return:
"""
Parameters
----------
candles: list[dict[str, float]]
start_dt: datetime
end_dt: datetime
data_frequency: str
field: str
previous_value: float
Returns
-------
Series
"""
dates = [candle['last_traded'] for candle in candles]
values = [candle[field] for candle in candles]
periods = self.bundle.get_calendar_periods_range(
start_dt, end_dt, data_frequency
)
series = pd.Series(values, index=dates)
#TODO: ensure that this working as expected, if not use fillna
series.reindex(periods, method='ffill', fill_value=previous_value)
periods = get_periods_range(
start_dt, end_dt, data_frequency
)
# TODO: ensure that this working as expected, if not use fillna
series = series.reindex(
periods,
method='ffill',
fill_value=previous_value,
)
return series
def get_history_window(self,
assets,
end_dt,
bar_count,
frequency,
field,
data_frequency=None,
ffill=True):
@deprecated
def get_history_window_direct(self,
assets,
end_dt,
bar_count,
frequency,
field,
data_frequency=None,
ffill=True):
"""
Public API method that returns a dataframe containing the requested
@@ -413,10 +484,11 @@ class Exchange:
Parameters
----------
assets : list of catalyst.data.Asset objects
assets : list[TradingPair]
The assets whose data is desired.
end_dt: not applicable to cryptocurrencies
end_dt: datetime
The date of the last bar
bar_count: int
The number of bars desired.
@@ -438,28 +510,79 @@ class Exchange:
Returns
-------
A dataframe containing the requested data.
DataFrame
A dataframe containing the requested data.
"""
start_dt = get_start_dt(end_dt, bar_count, data_frequency)
freq_match = re.match(r'([0-9].*)(m|M|d|D)', frequency, re.M | re.I)
if freq_match:
candle_size = int(freq_match.group(1))
unit = freq_match.group(2)
# The get_history method supports multiple asset
candles = self.get_candles(
data_frequency=frequency,
assets=assets,
bar_count=bar_count,
start_dt=start_dt,
end_dt=end_dt
)
candle_series = self.get_series_from_candles(
candles=candles,
start_dt=start_dt,
end_dt=end_dt,
data_frequency=frequency,
field=field,
)
else:
raise InvalidHistoryFrequencyError(frequency)
df = pd.DataFrame(candle_series)
return df
if unit.lower() == 'd':
if data_frequency == 'minute':
data_frequency = 'daily'
def get_history_window(self,
assets,
end_dt,
bar_count,
frequency,
field,
data_frequency=None,
ffill=True):
elif unit.lower() == 'm':
if data_frequency == 'daily':
data_frequency = 'minute'
"""
Public API method that returns a dataframe containing the requested
history window. Data is fully adjusted.
else:
raise InvalidHistoryFrequencyError(frequency)
Parameters
----------
assets : list[TradingPair]
The assets whose data is desired.
end_dt: datetime
The date of the last bar.
bar_count: int
The number of bars desired.
frequency: string
"1d" or "1m"
field: string
The desired field of the asset.
data_frequency: string
The frequency of the data to query; i.e. whether the data is
'daily' or 'minute' bars.
# TODO: fill how?
ffill: boolean
Forward-fill missing values. Only has effect if field
is 'price'.
Returns
-------
DataFrame
A dataframe containing the requested data.
"""
freq, candle_size, unit, data_frequency = get_frequency(
frequency, data_frequency
)
adj_bar_count = candle_size * bar_count
try:
series = self.bundle.get_history_window_series_and_load(
@@ -469,7 +592,7 @@ class Exchange:
field=field,
data_frequency=data_frequency
)
except PricingDataNotLoadedError:
except (PricingDataNotLoadedError, NoDataAvailableOnExchange):
series = dict()
for asset in assets:
@@ -482,12 +605,14 @@ class Exchange:
series[asset].index[-1] + get_delta(1, data_frequency) \
if asset in series else start_dt
trailing_bar_count = \
get_periods(trailing_dt, end_dt, data_frequency)
# The get_history method supports multiple asset
# Use the original frequency to let each api optimize
# the size of result sets
trailing_bar_count = get_periods(
trailing_dt, end_dt, freq
)
candles = self.get_candles(
data_frequency=data_frequency,
freq=freq,
assets=asset,
bar_count=trailing_bar_count,
start_dt=start_dt,
@@ -497,6 +622,8 @@ class Exchange:
last_value = series[asset].iloc(0) if asset in series \
else np.nan
# Create a series with the common data_frequency, ffill
# missing values
candle_series = self.get_series_from_candles(
candles=candles,
start_dt=trailing_dt,
@@ -512,23 +639,9 @@ class Exchange:
else:
series[asset] = candle_series
df = pd.DataFrame(series)
if candle_size > 1:
if field == 'open':
agg = 'first'
elif field == 'high':
agg = 'max'
elif field == 'low':
agg = 'min'
elif field == 'close':
agg = 'last'
elif field == 'volume':
agg = 'sum'
else:
raise ValueError('Invalid field.')
df = df.resample('{}T'.format(candle_size)).agg(agg)
df = resample_history_df(pd.DataFrame(series), freq, field)
# TODO: consider this more carefully
df.dropna(inplace=True)
return df
@@ -537,7 +650,6 @@ class Exchange:
Update the portfolio cash and position balances based on the
latest ticker prices.
:return:
"""
log.debug('synchronizing portfolio with exchange {}'.format(self.name))
balances = self.get_balances()
@@ -581,16 +693,20 @@ class Exchange:
Parameters
----------
asset : Asset
asset : TradingPair
The asset that this order is for.
amount : int
The amount of shares to order. If ``amount`` is positive, this is
the number of shares to buy or cover. If ``amount`` is negative,
this is the number of shares to sell or short.
limit_price : float, optional
The limit price for the order.
stop_price : float, optional
The stop price for the order.
style : ExecutionStyle, optional
The execution style for the order.
@@ -615,6 +731,7 @@ class Exchange:
:class:`catalyst.finance.execution.ExecutionStyle`
:func:`catalyst.api.order_value`
:func:`catalyst.api.order_percent`
"""
if amount == 0:
log.warn('skipping order amount of 0')
@@ -664,8 +781,12 @@ class Exchange:
@abstractmethod
def get_balances(self):
"""
Retrieve wallet balances for the exchange
:return balances: A dict of currency => available balance
Retrieve wallet balances for the exchange.
Returns
-------
dict[TradingPair, float]
"""
pass
@@ -674,17 +795,25 @@ class Exchange:
"""
Place an order on the exchange.
:param asset : Asset
The asset that this order is for.
:param amount : int
Parameters
----------
asset: TradingPair
The target market.
amount: float
The amount of shares to order. If ``amount`` is positive, this is
the number of shares to buy or cover. If ``amount`` is negative,
this is the number of shares to sell or short.
:param style : ExecutionStyle
The execution style for the order.
:param is_buy: boolean
is_buy: bool
Is it a buy order?
:return:
style: ExecutionStyle
Returns
-------
Order
"""
pass
@@ -739,23 +868,32 @@ class Exchange:
pass
@abstractmethod
def get_candles(self, data_frequency, assets, bar_count=None,
def get_candles(self, freq, assets, bar_count=None,
start_dt=None, end_dt=None):
"""
Retrieve OHLCV candles for the given assets
:param data_frequency:
The candle frequency: minute or daily
:param assets: list[TradingPair]
Parameters
----------
freq: str
The frequency alias per convention:
http://pandas.pydata.org/pandas-docs/stable/timeseries.html#offset-aliases
assets: list[TradingPair]
The targeted assets.
:param bar_count:
bar_count: int
The number of bar desired. (default 1)
:param end_dt: datetime, optional
end_dt: datetime, optional
The last bar date.
:param start_dt: datetime, optional
start_dt: datetime, optional
The first bar date.
:return dict[TradingPair, dict[str, Object]]: OHLCV data
Returns
-------
dict[TradingPair, dict[str, Object]]
A dictionary of OHLCV candles. Each TradingPair instance is
mapped to a list of dictionaries with this structure:
open: float
@@ -775,8 +913,14 @@ class Exchange:
"""
Retrieve current tick data for the given assets
:param assets:
:return:
Parameters
----------
assets: list[TradingPair]
Returns
-------
list[dict[str, float]
"""
pass
@@ -784,7 +928,6 @@ class Exchange:
def get_account(self):
"""
Retrieve the account parameters.
:return:
"""
pass
@@ -793,11 +936,15 @@ class Exchange:
"""
Retrieve the the orderbook for the given trading pair.
:param asset: TradingPair
:param order_type: str
Parameters
----------
asset: TradingPair
order_type: str
The type of orders: bid, ask or all
:param limit
limit: int
:return:
Returns
-------
list[dict[str, float]
"""
pass
+110 -8
View File
@@ -127,7 +127,13 @@ class ExchangeTradingAlgorithmBase(TradingAlgorithm):
"""
Creates a dictionary representing the state of the tracker.
Parameters
----------
start_dt: datetime
end_dt: datetime
Notes
-----
I rewrote this in an attempt to better control the stats.
I don't want things to happen magically through complex logic
pertaining to backtesting.
@@ -296,6 +302,18 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
self.exchange.minute_reader = BcolzMinuteBarReader(root)
def signal_handler(self, signal, frame):
"""
Handles the keyboard interruption signal.
Parameters
----------
signal
frame
Returns
-------
"""
self.is_running = False
if self._analyze is None:
@@ -384,7 +402,11 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
"""
We skip the entire performance tracker business and update the
portfolio directly.
:return:
Returns
-------
ExchangePortfolio
"""
# TODO: build cumulative portfolio
return self.perf_tracker.get_portfolio(False)
@@ -450,6 +472,17 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
)
def add_pnl_stats(self, period_stats):
"""
Save p&l stats.
Parameters
----------
period_stats
Returns
-------
"""
starting = period_stats['starting_cash']
current = period_stats['portfolio_value']
appreciation = (current / starting) - 1
@@ -466,6 +499,17 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
save_algo_df(self.algo_namespace, 'pnl_stats', self.pnl_stats)
def add_custom_signals_stats(self, period_stats):
"""
Save custom signals stats.
Parameters
----------
period_stats
Returns
-------
"""
log.debug('adding custom signals stats: {}'.format(self.recorded_vars))
df = pd.DataFrame(
data=[self.recorded_vars],
@@ -477,6 +521,17 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
self.custom_signals_stats)
def add_exposure_stats(self, period_stats):
"""
Save exposure stats.
Parameters
----------
period_stats
Returns
-------
"""
data = dict(
long_exposure=period_stats['long_exposure'],
base_currency=period_stats['ending_cash']
@@ -493,6 +548,14 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
self.exposure_stats)
def handle_data(self, data):
"""
Wrapper around the handle_data method of each algo.
Parameters
----------
data
"""
if not self.is_running:
return
@@ -619,15 +682,16 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
The cumulative portfolio does not contain open orders but exchange
portfolios do.
:param asset: TradingPair
:param amount: float
:param limit_price: float
:param stop_price: float
:param style: Style
:return order: Order
Parameters
----------
asset: TradingPair
amount: float
limit_price: float
stop_price: float
style: Style
order: Order
The catalyst order object or None
"""
amount, style = self._calculate_order(asset, amount,
limit_price, stop_price,
style)
@@ -689,15 +753,53 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
'get_open_orders. Use `asset` instead.')
@api_method
def get_open_orders(self, asset=None):
"""Retrieve all of the current open orders.
Parameters
----------
asset : Asset
If passed and not None, return only the open orders for the given
asset instead of all open orders.
Returns
-------
open_orders : dict[list[Order]] or list[Order]
If no asset is passed this will return a dict mapping Assets
to a list containing all the open orders for the asset.
If an asset is passed then this will return a list of the open
orders for this asset.
"""
return self._get_open_orders(asset)
@api_method
def get_order(self, order_id, exchange_name):
"""Lookup an order based on the order id returned from one of the
order functions.
Parameters
----------
order_id : str
The unique identifier for the order.
Returns
-------
order : Order
The order object.
execution_price: float
The execution price per share of the order
"""
exchange = self.exchanges[exchange_name]
return exchange.get_order(order_id)
@api_method
def cancel_order(self, order_param, exchange_name):
"""Cancel an open order.
Parameters
----------
order_param : str or Order
The order_id or order object to cancel.
"""
exchange = self.exchanges[exchange_name]
order_id = order_param
+18 -10
View File
@@ -39,17 +39,25 @@ class BcolzExchangeBarReader(BcolzMinuteBarReader):
return self._data_frequency
def load_raw_arrays(self, fields, start_dt, end_dt, sids):
"""
Parameters
----------
fields : list of str
'open', 'high', 'low', 'close', or 'volume'
start_dt: Timestamp
Beginning of the window range.
end_dt: Timestamp
End of the window range.
sids : list of int
The asset identifiers in the window.
# if self._data_frequency == 'minute':
# return super(BcolzExchangeBarReader, self) \
# .load_raw_arrays(fields, start_dt, end_dt, sids)
#
# else:
# return self._load_daily_raw_arrays(fields, start_dt, end_dt, sids)
return self._load_raw_arrays(fields, start_dt, end_dt, sids)
def _load_raw_arrays(self, fields, start_dt, end_dt, sids):
Returns
-------
list of np.ndarray
A list with an entry per field of ndarrays with shape
(minutes in range, sids) with a dtype of float64, containing the
values for the respective field over start and end dt range.
"""
start_idx = self._find_position_of_minute(start_dt)
end_idx = self._find_position_of_minute(end_dt)
+7 -11
View File
@@ -5,16 +5,16 @@ from catalyst.constants import LOG_LEVEL
from catalyst.finance.blotter import Blotter
from catalyst.finance.commission import CommissionModel
from catalyst.finance.slippage import SlippageModel
from catalyst.finance.transaction import Transaction
from catalyst.finance.transaction import create_transaction
log = Logger('exchange_blotter', level=LOG_LEVEL)
# It seems like we need to accept greater slippage risk in cryptos
# Orders won't often close at Equity levels.
# TODO: consider adjusting dynamically based on trading pair
DEFAULT_SLIPPAGE_SPREAD = 0.02
DEFAULT_MAKER_FEE = 0.001
DEFAULT_TAKER_FEE = 0.002
# TODO: should work with set_commission and set_slippage
DEFAULT_SLIPPAGE_SPREAD = 0.0001
DEFAULT_MAKER_FEE = 0.0015
DEFAULT_TAKER_FEE = 0.0025
class TradingPairFeeSchedule(CommissionModel):
@@ -97,12 +97,8 @@ class TradingPairFixedSlippage(SlippageModel):
execution_price, execution_volume = self.process_order(data, order)
transaction = Transaction(
asset=order.asset,
amount=abs(execution_volume),
dt=dt,
price=execution_price,
order_id=order.id
transaction = create_transaction(
order, dt, execution_price, execution_volume
)
self._volume_for_bar += abs(transaction.amount)
+247 -168
View File
@@ -1,9 +1,14 @@
import os
import os
import shutil
from datetime import timedelta
from itertools import chain
import pandas as pd
from catalyst.assets._assets import TradingPair
from logbook import Logger
from pandas.tslib import Timestamp
from pytz import UTC
from six import itervalues
from catalyst import get_calendar
from catalyst.constants import LOG_LEVEL
@@ -11,11 +16,11 @@ from catalyst.data.minute_bars import BcolzMinuteOverlappingData, \
BcolzMinuteBarMetadata
from catalyst.exchange.bundle_utils import range_in_bundle, \
get_bcolz_chunk, get_delta, get_month_start_end, \
get_year_start_end, get_df_from_arrays, get_start_dt
get_year_start_end, get_df_from_arrays, get_start_dt, get_period_label
from catalyst.exchange.exchange_bcolz import BcolzExchangeBarReader, \
BcolzExchangeBarWriter
from catalyst.exchange.exchange_errors import EmptyValuesInBundleError, \
InvalidHistoryFrequencyError, TempBundleNotFoundError, \
TempBundleNotFoundError, \
NoDataAvailableOnExchange, \
PricingDataNotLoadedError
from catalyst.exchange.exchange_utils import get_exchange_folder
@@ -54,7 +59,10 @@ class ExchangeBundle:
"""
Get a data writer object, either a new object or from cache
:return: BcolzMinuteBarReader or BcolzDailyBarReader
Returns
-------
BcolzMinuteBarReader | BcolzDailyBarReader
"""
if path is None:
root = get_exchange_folder(self.exchange.name)
@@ -83,7 +91,10 @@ class ExchangeBundle:
"""
Get a data writer object, either a new object or from cache
:return: BcolzMinuteBarWriter or BcolzDailyBarWriter
Returns
-------
BcolzMinuteBarWriter | BcolzDailyBarWriter
"""
root = get_exchange_folder(self.exchange.name)
path = BUNDLE_NAME_TEMPLATE.format(
@@ -139,13 +150,19 @@ class ExchangeBundle:
If the data exists, the chunk ingestion is complete.
If any data is missing we ingest the data.
:param assets: list[TradingPair]
Parameters
----------
assets: list[TradingPair]
The assets is scope.
:param start_dt:
start_dt: datetime
The chunk start date.
:param end_dt:
end_dt: datetime
The chunk end date.
:return: list[TradingPair]
data_frequency: str
Returns
-------
list[TradingPair]
The assets missing from the bundle
"""
reader = self.get_reader(data_frequency)
@@ -159,13 +176,6 @@ class ExchangeBundle:
return missing_assets
def _write(self, data, writer, data_frequency):
"""
Write data to the writer
:param df:
:param writer:
:return:
"""
try:
writer.write(
data=data,
@@ -190,6 +200,20 @@ class ExchangeBundle:
)
def get_calendar_periods_range(self, start_dt, end_dt, data_frequency):
"""
Get a list of dates for the specified range.
Parameters
----------
start_dt: datetime
end_dt: datetime
data_frequency: str
Returns
-------
list[datetime]
"""
return self.calendar.minutes_in_range(start_dt, end_dt) \
if data_frequency == 'minute' \
else self.calendar.sessions_in_range(start_dt, end_dt)
@@ -199,13 +223,14 @@ class ExchangeBundle:
"""
Ingest a DataFrame of OHLCV data for a given market.
:param ohlcv_df:
:param data_frequency:
:param asset:
:param writer:
:param path:
:param empty_rows_behavior:
:return:
Parameters
----------
ohlcv_df: DataFrame
data_frequency: str
asset: TradingPair
writer:
empty_rows_behavior: str
"""
if empty_rows_behavior is not 'ignore':
nan_rows = ohlcv_df[ohlcv_df.isnull().T.any().T].index
@@ -259,19 +284,21 @@ class ExchangeBundle:
self._write(data, writer, data_frequency)
def ingest_ctable(self, asset, data_frequency, period, start_dt, end_dt,
def ingest_ctable(self, asset, data_frequency, period,
writer, empty_rows_behavior='strip', cleanup=False):
"""
Merge a ctable bundle chunk into the main bundle for the exchange.
:param asset: TradingPair
:param data_frequency: str
:param period: str
:param writer:
:param empty_rows_behavior: str
Parameters
----------
asset: TradingPair
data_frequency: str
period: str
writer:
empty_rows_behavior: str
Ensure that the bundle does not have any missing data.
:param cleanup: bool
cleanup: bool
Remove the temp bundle directory after ingestion.
:return:
@@ -288,6 +315,12 @@ class ExchangeBundle:
if reader is None:
raise TempBundleNotFoundError(path=path)
start_dt = reader.first_trading_day
end_dt = reader.last_available_dt
if data_frequency == 'daily':
end_dt = end_dt - pd.Timedelta(hours=23, minutes=59)
arrays = None
try:
arrays = reader.load_raw_arrays(
@@ -326,13 +359,19 @@ class ExchangeBundle:
def get_adj_dates(self, start, end, assets, data_frequency):
"""
Contains a date range to the trading availability of the specified pairs.
Contains a date range to the trading availability of the specified
markets.
:param start:
:param end:
:param assets:
:param data_frequency:
:return:
Parameters
----------
start: datetime
end: datetime
assets: list[TradingPair]
data_frequency: str
Returns
-------
datetime, datetime
"""
earliest_trade = None
last_entry = None
@@ -375,15 +414,27 @@ class ExchangeBundle:
Split a price data request into chunks corresponding to individual
bundles.
:param assets:
:param data_frequency:
:param start_dt:
:param end_dt:
:return:
Parameters
----------
assets: list[TradingPair]
data_frequency: str
start_dt: datetime
end_dt: datetime
Returns
-------
dict[TradingPair, list[dict(str, Object]]]
"""
get_start_end = get_month_start_end \
if data_frequency == 'minute' else get_year_start_end
start_dt, _ = get_start_end(start_dt)
_, end_dt = get_start_end(end_dt)
reader = self.get_reader(data_frequency)
chunks = []
chunks = dict()
for asset in assets:
try:
# Checking if the the asset has price data in the specified
@@ -397,106 +448,75 @@ class ExchangeBundle:
log.debug('skipping {}: {}'.format(asset.symbol, e))
continue
# This is either the first trading day of the asset or the
# first session available in the calendar
first_trading_dt = asset.start_date \
if asset.start_date > self.calendar.first_session \
else self.calendar.first_session
dates = pd.date_range(
start=get_period_label(adj_start, data_frequency),
end=get_period_label(adj_end, data_frequency),
freq='MS' if data_frequency == 'minute' else 'AS',
tz=UTC
)
# Aligning start / end dates with the daily calendar
sessions = self.calendar.sessions_in_range(adj_start, adj_end)
# Adjusting the last date of the range to avoid
# going over the asset's trading bounds
dates.values[0] = adj_start
dates.values[-1] = adj_end
# We loop through each session to create chunks for each period
chunk_labels = []
dt = sessions[0]
while dt <= sessions[-1]:
label = '{}-{:02d}'.format(dt.year, dt.month) \
if data_frequency == 'minute' else '{}'.format(dt.year)
chunks[asset] = []
for index, dt in enumerate(dates):
if label not in chunk_labels:
chunk_labels.append(label)
period_start, period_end = get_start_end(
dt=dt,
first_day=dt if index == 0 else None,
last_day=dt if index == len(dates) - 1 else None
)
# Adjusting the period dates to match the availability
# of the trading pair
if data_frequency == 'minute':
period_start, period_end = get_month_start_end(dt)
# Currencies don't always start trading at midnight.
# Checking the last minute of the day instead.
range_start = period_start.replace(hour=23, minute=59) \
if data_frequency == 'minute' else period_start
asset_start_month, _ = get_month_start_end(
first_trading_dt
# Checking if the data already exists in the bundle
# for the date range of the chunk. If not, we create
# a chunk for ingestion.
has_data = range_in_bundle(
asset, range_start, period_end, reader
)
if not has_data:
chunks[asset].append(
dict(
asset=asset,
period_start=period_start,
period_end=period_end,
period=get_period_label(dt, data_frequency)
)
if asset_start_month == period_start \
and period_start < first_trading_dt:
period_start = first_trading_dt
# TODO: need to filter closed pairs?
_, asset_end_month = get_month_start_end(
asset.end_minute
)
if asset_end_month == period_end \
and period_end > asset.end_minute:
period_end = asset.end_minute
elif data_frequency == 'daily':
period_start, period_end = get_year_start_end(dt)
asset_start_year, _ = get_year_start_end(
first_trading_dt
)
if asset_start_year == period_start \
and period_start < first_trading_dt:
period_start = first_trading_dt
_, asset_end_year = get_year_start_end(
asset.end_daily
)
if asset_end_year == period_end \
and period_end > asset.end_daily:
period_end = asset.end_daily
else:
raise InvalidHistoryFrequencyError(
frequency=data_frequency
)
# Currencies don't always start trading at midnight.
# Checking the last minute of the day instead.
range_start = period_start.replace(hour=23, minute=59) \
if data_frequency == 'minute' else period_start
# Checking if the data already exists in the bundle
# for the date range of the chunk. If not, we create
# a chunk for ingestion.
has_data = range_in_bundle(
asset, range_start, period_end, reader
)
if not has_data:
log.debug('adding period: {}'.format(label))
chunks.append(
dict(
asset=asset,
period_start=period_start,
period_end=period_end,
period=label
)
)
dt += timedelta(days=1)
# We sort the chunks by end date to ingest most recent data first
chunks.sort(key=lambda chunk: chunk['period_end'])
# We sort the chunks by end date to ingest most recent data first
chunks[asset].sort(key=lambda chunk: chunk['period_end'])
return chunks
def ingest_assets(self, assets, start_dt, end_dt, data_frequency,
show_progress=False):
def ingest_assets(self, assets, data_frequency, start_dt=None, end_dt=None,
show_progress=False, asset_chunks=False):
"""
Determine if data is missing from the bundle and attempt to ingest it.
:param assets:
:param start_dt:
:param end_dt:
:return:
Parameters
----------
assets: list[TradingPair]
start_dt: datetime
end_dt: datetime
"""
if start_dt is None:
start_dt = self.calendar.first_session
if end_dt is None:
end_dt = pd.Timestamp.utcnow()
start_dt, end_dt = self.get_adj_dates(
start_dt, end_dt, assets, data_frequency
)
chunks = self.prepare_chunks(
assets=assets,
data_frequency=data_frequency,
@@ -507,7 +527,8 @@ class ExchangeBundle:
# Since chunks are either monthly or yearly, it is possible that
# our ingestion data range is greater than specified. We adjust
# the boundaries to ensure that the writer can write all data.
for chunk in chunks:
all_chunks = list(chain.from_iterable(itervalues(chunks)))
for chunk in all_chunks:
if chunk['period_start'] < start_dt:
start_dt = chunk['period_start']
@@ -515,54 +536,94 @@ class ExchangeBundle:
end_dt = chunk['period_end']
writer = self.get_writer(start_dt, end_dt, data_frequency)
with maybe_show_progress(
chunks,
show_progress,
label='Fetching {exchange} {frequency} candles: '.format(
exchange=self.exchange.name,
frequency=data_frequency
)) as it:
for chunk in it:
self.ingest_ctable(
asset=chunk['asset'],
data_frequency=data_frequency,
period=chunk['period'],
start_dt=chunk['period_start'],
end_dt=chunk['period_end'],
writer=writer,
empty_rows_behavior='strip',
cleanup=True
)
if asset_chunks:
for asset in chunks:
with maybe_show_progress(
chunks[asset],
show_progress,
label='Ingesting {frequency} price data for '
'{symbol} on {exchange}'.format(
exchange=self.exchange.name,
frequency=data_frequency,
symbol=asset.symbol
)) as it:
for chunk in it:
self.ingest_ctable(
asset=chunk['asset'],
data_frequency=data_frequency,
period=chunk['period'],
writer=writer,
empty_rows_behavior='strip',
cleanup=True
)
else:
with maybe_show_progress(
all_chunks,
show_progress,
label='Ingesting {frequency} price data on '
'{exchange}'.format(
exchange=self.exchange.name,
frequency=data_frequency,
)) as it:
for chunk in it:
self.ingest_ctable(
asset=chunk['asset'],
data_frequency=data_frequency,
period=chunk['period'],
writer=writer,
empty_rows_behavior='strip',
cleanup=True
)
def ingest(self, data_frequency, include_symbols=None,
exclude_symbols=None, start=None, end=None,
show_progress=True, environ=os.environ):
"""
Inject data based on specified parameters.
Parameters
----------
data_frequency: str
include_symbols: str
exclude_symbols: str
start: datetime
end: datetime
show_progress: bool
environ:
:param data_frequency:
:param include_symbols:
:param exclude_symbols:
:param start:
:param end:
:param show_progress:
:param environ:
:return:
"""
assets = self.get_assets(include_symbols, exclude_symbols)
start_dt, end_dt = self.get_adj_dates(
start, end, assets, data_frequency
)
for frequency in data_frequency.split(','):
self.ingest_assets(assets, start_dt, end_dt, frequency,
self.ingest_assets(assets, frequency, start, end,
show_progress)
def get_history_window_series_and_load(self,
assets,
end_dt,
bar_count,
field,
data_frequency):
assets, # type: List[TradingPair]
end_dt, # type: Timestamp
bar_count, # type: int
field, # type: str
data_frequency, # type: str
algo_end_dt=None # type: Timestamp
):
"""
Retrieve price data history, ingest missing data.
Parameters
----------
assets: list[TradingPair]
end_dt: datetime
bar_count: int
field: str
data_frequency: str
algo_end_dt: datetime
Returns
-------
Series
"""
try:
series = self.get_history_window_series(
assets=assets,
@@ -586,9 +647,10 @@ class ExchangeBundle:
self.ingest_assets(
assets=assets,
start_dt=start_dt,
end_dt=end_dt,
end_dt=algo_end_dt,
data_frequency=data_frequency,
show_progress=True
show_progress=True,
asset_chunks=True
)
series = self.get_history_window_series(
assets=assets,
@@ -596,12 +658,29 @@ class ExchangeBundle:
bar_count=bar_count,
field=field,
data_frequency=data_frequency,
reset_reader=True
reset_reader=False
)
return series
def get_spot_values(self, assets, field, dt, data_frequency,
reset_reader=False):
def get_spot_values(self,
assets, # type: List[TradingPair]
field, # type: str
dt, # type: Timestamp
data_frequency, # type: str
reset_reader=False # type: bool
):
# type: (...) -> List[float]
"""
The spot values for the gives assets, field and date. Reads from
the exchange data bundle.
:param assets:
:param field:
:param dt:
:param data_frequency:
:param reset_reader:
:return:
"""
values = []
try:
reader = self.get_reader(data_frequency)
@@ -1,16 +1,3 @@
#
# 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.
import abc
from time import sleep
@@ -26,6 +13,7 @@ from catalyst.exchange.exchange_errors import (
ExchangeRequestError,
ExchangeBarDataError,
PricingDataNotLoadedError)
from catalyst.exchange.exchange_utils import get_frequency, resample_history_df
log = Logger('DataPortalExchange', level=LOG_LEVEL)
@@ -237,6 +225,25 @@ class DataPortalExchangeLive(DataPortalExchangeBase):
field,
data_frequency,
ffill=True):
"""
Fetching price history window from the exchange.
Parameters
----------
exchange: Exchange
assets: list[TradingPair]
end_dt: datetime
bar_count: int
frequency: str
field: str
data_frequency: str
ffill: bool
Returns
-------
DataFrame
"""
df = exchange.get_history_window(
assets,
end_dt,
@@ -249,6 +256,22 @@ class DataPortalExchangeLive(DataPortalExchangeBase):
def get_exchange_spot_value(self, exchange, assets, field, dt,
data_frequency):
"""
A spot value for the exchange.
Parameters
----------
exchange: Exchange
assets: list[TradingPair]
field: str
dt: datetime
data_frequency: str
Returns
-------
float
"""
exchange_spot_values = exchange.get_spot_value(
assets, field, dt, data_frequency)
@@ -287,34 +310,69 @@ class DataPortalExchangeBacktest(DataPortalExchangeBase):
"""
Fetching price history window from the exchange bundle.
Using a try... except approach to minimize reads most of the time,
when the data exists.
Parameters
----------
exchange: Exchange
assets: list[TradingPair]
end_dt: datetime
bar_count: int
frequency: str
field: str
data_frequency: str
ffill: bool
Returns
-------
DataFrame
:param exchange:
:param assets:
:param end_dt:
:param bar_count:
:param frequency:
:param field:
:param data_frequency:
:param ffill:
:return:
"""
bundle = self.exchange_bundles[exchange.name] # type: ExchangeBundle
freq, candle_size, unit, adj_data_frequency = get_frequency(
frequency, data_frequency
)
adj_bar_count = candle_size * bar_count
if data_frequency == 'minute' and adj_data_frequency == 'daily':
end_dt = end_dt.floor('1D')
bundle = self.exchange_bundles[exchange.name]
series = bundle.get_history_window_series_and_load(
assets=assets,
end_dt=end_dt,
bar_count=bar_count,
bar_count=adj_bar_count,
field=field,
data_frequency=data_frequency
data_frequency=adj_data_frequency,
algo_end_dt=self._last_available_session,
)
return pd.DataFrame(series)
def get_exchange_spot_value(self, exchange, assets, field, dt,
data_frequency):
df = resample_history_df(pd.DataFrame(series), freq, field)
return df
def get_exchange_spot_value(self,
exchange,
assets,
field,
dt,
data_frequency
):
"""
A spot value for the exchange bundle. Try to ingest data if not in
the bundle.
Parameters
----------
exchange: Exchange
assets: list[TradingPair]
field: str
dt: datetime
data_frequency: str
Returns
-------
float
"""
bundle = self.exchange_bundles[exchange.name]
if data_frequency == 'daily':
dt = dt.floor('1D')
else:
+8
View File
@@ -86,6 +86,14 @@ class AlgoPickleNotFound(ZiplineError):
).strip()
class InvalidHistoryFrequencyAlias(ZiplineError):
msg = (
'Invalid frequency alias {freq}. Valid suffixes are M (minute) '
'and D (day). For example, these aliases would be valid '
'1M, 5M, 1D.'
).strip()
class InvalidHistoryFrequencyError(ZiplineError):
msg = (
'Frequency {frequency} not supported by the exchange.'
+40 -12
View File
@@ -4,9 +4,16 @@ from catalyst.finance.execution import LimitOrder, StopOrder, StopLimitOrder
class ExchangeLimitOrder(LimitOrder):
def get_limit_price(self, is_buy):
"""
We may be trading Satoshis with 8 decimals, we cannot round numbers
:param is_buy:
:return:
We may be trading Satoshis with 8 decimals, we cannot round numbers.
Parameters
----------
is_buy: bool
Returns
-------
float
"""
return self.limit_price
@@ -14,9 +21,16 @@ class ExchangeLimitOrder(LimitOrder):
class ExchangeStopOrder(StopOrder):
def get_stop_price(self, is_buy):
"""
We may be trading Satoshis with 8 decimals, we cannot round numbers
:param is_buy:
:return:
We may be trading Satoshis with 8 decimals, we cannot round numbers.
Parameters
----------
is_buy: bool
Returns
-------
float
"""
return self.stop_price
@@ -24,16 +38,30 @@ class ExchangeStopOrder(StopOrder):
class ExchangeStopLimitOrder(StopLimitOrder):
def get_limit_price(self, is_buy):
"""
We may be trading Satoshis with 8 decimals, we cannot round numbers
:param is_buy:
:return:
We may be trading Satoshis with 8 decimals, we cannot round numbers.
Parameters
----------
is_buy: bool
Returns
-------
float
"""
return self.limit_price
def get_stop_price(self, is_buy):
"""
We may be trading Satoshis with 8 decimals, we cannot round numbers
:param is_buy:
:return:
We may be trading Satoshis with 8 decimals, we cannot round numbers.
Parameters
----------
is_buy: bool
Returns
-------
float
"""
return self.stop_price
+31 -3
View File
@@ -3,6 +3,7 @@ from logbook import Logger
from catalyst.constants import LOG_LEVEL
from catalyst.protocol import Portfolio, Positions, Position
from catalyst.utils.deprecate import deprecated
log = Logger('ExchangePortfolio', level=LOG_LEVEL)
@@ -29,10 +30,15 @@ class ExchangePortfolio(Portfolio):
self.positions_value = 0.0
self.open_orders = dict()
def calculate_pnl(self):
log.debug('calculating pnl')
def create_order(self, order):
"""
Create an open order and store in memory.
Parameters
----------
order: Order
"""
log.debug('creating order {}'.format(order.id))
self.open_orders[order.id] = order
@@ -47,6 +53,18 @@ class ExchangePortfolio(Portfolio):
log.debug('open order added to portfolio')
def execute_order(self, order, transaction):
"""
Update the open orders and positions to apply an executed order.
Unlike with backtesting, we do not need to add slippage and fees.
The executed price includes transaction fees.
Parameters
----------
order: Order
transaction: Transaction
"""
log.debug('executing order {}'.format(order.id))
del self.open_orders[order.id]
@@ -71,7 +89,9 @@ class ExchangePortfolio(Portfolio):
log.debug('updated portfolio with executed order')
@deprecated
def execute_transaction(self, transaction):
# TODO: almost duplicate of execute_order. Not sure why Poloniex needs this.
log.debug('executing transaction {}'.format(transaction.order_id))
order_position = self.positions[transaction.asset] \
@@ -96,6 +116,14 @@ class ExchangePortfolio(Portfolio):
log.debug('updated portfolio with executed order')
def remove_order(self, order):
"""
Removing an open order.
Parameters
----------
order: Order
"""
log.info('removing cancelled order {}'.format(order.id))
del self.open_orders[order.id]
+308 -14
View File
@@ -1,14 +1,15 @@
import json
import os
import pickle
from catalyst.assets._assets import TradingPair
from six.moves.urllib import request
import re
from datetime import date, datetime
import pandas as pd
from catalyst.assets._assets import TradingPair
from six.moves.urllib import request
from catalyst.exchange.exchange_errors import ExchangeSymbolsNotFound
from catalyst.exchange.exchange_errors import ExchangeSymbolsNotFound, \
InvalidHistoryFrequencyError, InvalidHistoryFrequencyAlias
from catalyst.utils.paths import data_root, ensure_directory, \
last_modified_time
@@ -17,6 +18,19 @@ SYMBOLS_URL = 'https://s3.amazonaws.com/enigmaco/catalyst-exchanges/' \
def get_exchange_folder(exchange_name, environ=None):
"""
The root path of an exchange folder.
Parameters
----------
exchange_name: str
environ:
Returns
-------
str
"""
if not environ:
environ = os.environ
@@ -28,11 +42,37 @@ def get_exchange_folder(exchange_name, environ=None):
def get_exchange_symbols_filename(exchange_name, environ=None):
"""
The absolute path of the exchange's symbol.json file.
Parameters
----------
exchange_name:
environ:
Returns
-------
str
"""
exchange_folder = get_exchange_folder(exchange_name, environ)
return os.path.join(exchange_folder, 'symbols.json')
def download_exchange_symbols(exchange_name, environ=None):
"""
Downloads the exchange's symbols.json from the repository.
Parameters
----------
exchange_name: str
environ:
Returns
-------
str
"""
filename = get_exchange_symbols_filename(exchange_name)
url = SYMBOLS_URL.format(exchange=exchange_name)
response = request.urlretrieve(url=url, filename=filename)
@@ -40,6 +80,19 @@ def download_exchange_symbols(exchange_name, environ=None):
def get_exchange_symbols(exchange_name, environ=None):
"""
The de-serialized content of the exchange's symbols.json.
Parameters
----------
exchange_name: str
environ:
Returns
-------
Object
"""
filename = get_exchange_symbols_filename(exchange_name)
if not os.path.isfile(filename) or \
@@ -60,11 +113,36 @@ def get_exchange_symbols(exchange_name, environ=None):
def get_symbols_string(assets):
"""
A concatenated string of symbols from a list of assets.
Parameters
----------
assets: list[TradingPair]
Returns
-------
str
"""
array = [assets] if isinstance(assets, TradingPair) else assets
return ', '.join([asset.symbol for asset in array])
def get_exchange_auth(exchange_name, environ=None):
"""
The de-serialized contend of the exchange's auth.json file.
Parameters
----------
exchange_name: str
environ:
Returns
-------
Object
"""
exchange_folder = get_exchange_folder(exchange_name, environ)
filename = os.path.join(exchange_folder, 'auth.json')
@@ -81,6 +159,19 @@ def get_exchange_auth(exchange_name, environ=None):
def get_algo_folder(algo_name, environ=None):
"""
The algorithm root folder of the algorithm.
Parameters
----------
algo_name: str
environ:
Returns
-------
str
"""
if not environ:
environ = os.environ
@@ -92,6 +183,21 @@ def get_algo_folder(algo_name, environ=None):
def get_algo_object(algo_name, key, environ=None, rel_path=None):
"""
The de-serialized object of the algo name and key.
Parameters
----------
algo_name: str
key: str
environ:
rel_path: str
Returns
-------
Object
"""
if algo_name is None:
return None
@@ -113,6 +219,18 @@ def get_algo_object(algo_name, key, environ=None, rel_path=None):
def save_algo_object(algo_name, key, obj, environ=None, rel_path=None):
"""
Serialize and save an object by algo name and key.
Parameters
----------
algo_name: str
key: str
obj: Object
environ:
rel_path: str
"""
folder = get_algo_folder(algo_name, environ)
if rel_path is not None:
@@ -125,16 +243,22 @@ def save_algo_object(algo_name, key, obj, environ=None, rel_path=None):
pickle.dump(obj, handle, protocol=pickle.HIGHEST_PROTOCOL)
def append_algo_object(algo_name, key, obj, environ=None):
algo_folder = get_algo_folder(algo_name, environ)
filename = os.path.join(algo_folder, key + '.p')
mode = 'a+b' if os.path.isfile(filename) else 'wb'
with open(filename, mode) as handle:
pickle.dump(obj, handle, protocol=pickle.HIGHEST_PROTOCOL)
def get_algo_df(algo_name, key, environ=None, rel_path=None):
"""
The de-serialized DataFrame of an algo name and key.
Parameters
----------
algo_name: str
key: str
environ:
rel_path: str
Returns
-------
DataFrame
"""
folder = get_algo_folder(algo_name, environ)
if rel_path is not None:
@@ -153,6 +277,18 @@ def get_algo_df(algo_name, key, environ=None, rel_path=None):
def save_algo_df(algo_name, key, df, environ=None, rel_path=None):
"""
Serialize to csv and save a DataFrame by algo name and key.
Parameters
----------
algo_name: str
key: str
df: DataFrame
environ:
rel_path: str
"""
folder = get_algo_folder(algo_name, environ)
if rel_path is not None:
@@ -166,6 +302,19 @@ def save_algo_df(algo_name, key, df, environ=None, rel_path=None):
def get_exchange_minute_writer_root(exchange_name, environ=None):
"""
The minute writer folder for the exchange.
Parameters
----------
exchange_name: str
environ:
Returns
-------
BcolzExchangeBarWriter
"""
exchange_folder = get_exchange_folder(exchange_name, environ)
minute_data_folder = os.path.join(exchange_folder, 'minute_data')
@@ -175,6 +324,19 @@ def get_exchange_minute_writer_root(exchange_name, environ=None):
def get_exchange_bundles_folder(exchange_name, environ=None):
"""
The temp folder for bundle downloads by algo name.
Parameters
----------
exchange_name: str
environ:
Returns
-------
str
"""
exchange_folder = get_exchange_folder(exchange_name, environ)
temp_bundles = os.path.join(exchange_folder, 'temp_bundles')
@@ -184,8 +346,140 @@ def get_exchange_bundles_folder(exchange_name, environ=None):
def perf_serial(obj):
"""JSON serializer for objects not serializable by default json code"""
"""
JSON serializer for objects not serializable by default json code
Parameters
----------
obj: Object
Returns
-------
str
"""
if isinstance(obj, (datetime, date)):
return obj.isoformat()
raise TypeError("Type %s not serializable" % type(obj))
def get_common_assets(exchanges):
"""
The assets available in all specified exchanges.
Parameters
----------
exchanges: list[Exchange]
Returns
-------
list[TradingPair]
"""
symbols = []
for exchange_name in exchanges:
s = [asset.symbol for asset in exchanges[exchange_name].get_assets()]
symbols.append(s)
inter_symbols = set.intersection(*map(set, symbols))
assets = []
for symbol in inter_symbols:
for exchange_name in exchanges:
asset = exchanges[exchange_name].get_asset(symbol)
assets.append(asset)
return assets
def get_frequency(freq, data_frequency):
"""
Get the frequency parameters.
Notes
-----
We're trying to use Pandas convention for frequency aliases.
Parameters
----------
freq: str
data_frequency: str
Returns
-------
str, int, str, str
"""
if freq == 'minute':
unit = 'T'
candle_size = 1
elif freq == 'daily':
unit = 'D'
candle_size = 1
else:
freq_match = re.match(r'([0-9].*)?(m|M|d|D|h|H|T)', freq, re.M | re.I)
if freq_match:
candle_size = int(freq_match.group(1)) if freq_match.group(1) \
else 1
unit = freq_match.group(2)
else:
raise InvalidHistoryFrequencyError(frequency=freq)
if unit.lower() == 'd':
alias = '{}D'.format(candle_size)
if data_frequency == 'minute':
data_frequency = 'daily'
elif unit.lower() == 'm' or unit == 'T':
alias = '{}T'.format(candle_size)
if data_frequency == 'daily':
data_frequency = 'minute'
# elif unit.lower() == 'h':
# candle_size = candle_size * 60
#
# alias = '{}T'.format(candle_size)
# if data_frequency == 'daily':
# data_frequency = 'minute'
else:
raise InvalidHistoryFrequencyAlias(freq=freq)
return alias, candle_size, unit, data_frequency
def resample_history_df(df, freq, field):
"""
Resample the OHCLV DataFrame using the specified frequency.
Parameters
----------
df: DataFrame
freq: str
field: str
Returns
-------
DataFrame
"""
if field == 'open':
agg = 'first'
elif field == 'high':
agg = 'max'
elif field == 'low':
agg = 'min'
elif field == 'close':
agg = 'last'
elif field == 'volume':
agg = 'sum'
else:
raise ValueError('Invalid field.')
return df.resample(freq).agg(agg)
@@ -5,28 +5,39 @@ from catalyst.exchange.exchange_utils import get_exchange_auth
from catalyst.exchange.poloniex.poloniex import Poloniex
def get_exchange(exchange_name):
def get_exchange(exchange_name, base_currency=None):
exchange_auth = get_exchange_auth(exchange_name)
if exchange_name == 'bitfinex':
return Bitfinex(
key=exchange_auth['key'],
secret=exchange_auth['secret'],
base_currency=None, # TODO: make optional at the exchange
base_currency=base_currency,
portfolio=None
)
elif exchange_name == 'bittrex':
return Bittrex(
key=exchange_auth['key'],
secret=exchange_auth['secret'],
base_currency=None,
base_currency=base_currency,
portfolio=None
)
elif exchange_name == 'poloniex':
return Poloniex(
key=exchange_auth['key'],
secret=exchange_auth['secret'],
base_currency=None,
base_currency=base_currency,
portfolio=None
)
else:
raise ExchangeNotFoundError(exchange_name=exchange_name)
def get_exchanges(exchange_names):
exchanges = dict()
for exchange_name in exchange_names:
exchanges[exchange_name] = get_exchange(exchange_name)
return exchanges
+27 -19
View File
@@ -1,16 +1,3 @@
#
# 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.
import pandas as pd
from catalyst.gens.sim_engine import (
BAR,
@@ -33,8 +20,8 @@ class LiveGraphClock(object):
This mixes the clock with a live graph.
Note
----
Notes
-----
This seemingly awkward approach allows us to run the program using a single
thread. This is important because Matplotlib does not play nice with
multi-threaded environments. Zipline probably does not either.
@@ -53,7 +40,7 @@ class LiveGraphClock(object):
def __init__(self, sessions, context, time_skew=pd.Timedelta('0s')):
global mdates, plt #TODO: Could be cleaner
global mdates, plt # TODO: Could be cleaner
import matplotlib.dates as mdates
from matplotlib import pyplot as plt
from matplotlib import style
@@ -95,11 +82,12 @@ class LiveGraphClock(object):
"""
Trying to assign reasonable parameters to the time axis.
TODO: room for improvement
Parameters
----------
ax:
:param ax:
:return:
"""
# TODO: room for improvement
ax.xaxis.set_major_locator(mdates.DayLocator(interval=1))
ax.xaxis.set_major_formatter(self.fmt)
@@ -113,9 +101,21 @@ class LiveGraphClock(object):
ax.grid(True)
def set_legend(self, ax):
"""
Set legend on the chart.
Parameters
----------
ax
"""
ax.legend(loc='upper left', ncol=1, fontsize=10, numpoints=1)
def draw_pnl(self):
"""
Draw p&l line on the chart.
"""
ax = self.ax_pnl
df = self.context.pnl_stats
@@ -136,6 +136,10 @@ class LiveGraphClock(object):
self.format_ax(ax)
def draw_custom_signals(self):
"""
Draw custom signals on the chart.
"""
ax = self.ax_custom_signals
df = self.context.custom_signals_stats
@@ -154,6 +158,10 @@ class LiveGraphClock(object):
self.format_ax(ax)
def draw_exposure(self):
"""
Draw exposure line on the chart.
"""
ax = self.ax_exposure
context = self.context
df = context.exposure_stats
+21 -15
View File
@@ -1,3 +1,4 @@
import calendar
import json
import json
import time
@@ -171,12 +172,12 @@ class Poloniex(Exchange):
# TODO: fetch account data and keep in cache
return None
def get_candles(self, data_frequency, assets, bar_count=None,
def get_candles(self, freq, assets, bar_count=None,
start_dt=None, end_dt=None):
"""
Retrieve OHLVC candles from Poloniex
:param data_frequency:
:param freq:
:param assets:
:param bar_count:
:return:
@@ -193,31 +194,33 @@ class Poloniex(Exchange):
'retrieving {bars} {freq} candles on {exchange} from '
'{end_dt} for markets {symbols}, '.format(
bars=bar_count,
freq=data_frequency,
freq=freq,
exchange=self.name,
end_dt=end_dt,
symbols=get_symbols_string(assets)
)
)
if data_frequency == '5m':
if freq == '1T' and (bar_count == 1 or bar_count is None):
# TODO: use the order book instead
# We use the 5m to fetch the last bar
frequency = 300
elif data_frequency == '15m':
elif freq == '5T':
frequency = 300
elif freq == '15T':
frequency = 900
elif data_frequency == '30m':
elif freq == '30T':
frequency = 1800
elif data_frequency == '2h':
elif freq == '120T':
frequency = 7200
elif data_frequency == '4h':
elif freq == '240T':
frequency = 14400
elif data_frequency == '1D' or data_frequency == 'daily':
elif freq == '1D':
frequency = 86400
else:
# Poloniex does not offer 1m data candles
# It is likely to error out there frequently
raise InvalidHistoryFrequencyError(
frequency=data_frequency
)
raise InvalidHistoryFrequencyError(frequency=freq)
# Making sure that assets are iterable
asset_list = [assets] if isinstance(assets, TradingPair) else assets
@@ -225,15 +228,18 @@ class Poloniex(Exchange):
for asset in asset_list:
end = int(time.mktime(end_dt.timetuple()))
# TODO: what's wrong with this?
# end = int(time.mktime(end_dt.timetuple()))
end = int(time.time())
if bar_count is None:
start = end - 2 * frequency
else:
start = end - bar_count * frequency
try:
response = self.api.returnchartdata(self.get_symbol(asset),
frequency, start, end)
response = self.api.returnchartdata(
self.get_symbol(asset), frequency, start, end
)
except Exception as e:
raise ExchangeRequestError(error=e)
+74 -3
View File
@@ -1,14 +1,73 @@
import numpy as np
import pandas as pd
def crossover(source, target):
"""
The `x`-series is defined as having crossed over `y`-series if the value
of `x` is greater than the value of `y` and the value of `x` was less than
the value of `y` on the bar immediately preceding the current bar.
Parameters
----------
source: Series
target: Series
Returns
-------
bool
"""
if source[-1] is np.nan or source[-2] is np.nan \
or target[-1] is np.nan or target[-2] is np.nan:
return False
if source[-1] > target[-1] and source[-2] < target[-2]:
return True
else:
return False
def crossunder(source, target):
"""
The `x`-series is defined as having crossed under `y`-series if the value
of `x` is less than the value of `y` and the value of `x` was greater than
the value of `y` on the bar immediately preceding the current bar.
Parameters
----------
source: Series
target: Series
Returns
-------
bool
"""
if source[-1] is np.nan or source[-2] is np.nan \
or target[-1] is np.nan or target[-2] is np.nan:
return False
if source[-1] < target[-1] and source[-2] > target[-2]:
return True
else:
return False
def get_pretty_stats(stats_df, recorded_cols=None, num_rows=10):
"""
Format and print the last few rows of a statistics DataFrame.
See the pyfolio project for the data structure.
:param stats_df:
:param num_rows:
:return:
Parameters
----------
stats_df: DataFrame
num_rows: int
Returns
-------
str
"""
stats_df.set_index('period_close', drop=True, inplace=True)
stats_df.dropna(axis=1, how='all', inplace=True)
@@ -52,6 +111,18 @@ def get_pretty_stats(stats_df, recorded_cols=None, num_rows=10):
def df_to_string(df):
"""
Create a formatted str representation of the DataFrame.
Parameters
----------
df: DataFrame
Returns
-------
str
"""
pd.set_option('display.expand_frame_repr', False)
pd.set_option('precision', 8)
pd.set_option('display.width', 1000)
View File
+109
View File
@@ -0,0 +1,109 @@
import pandas as pd
from catalyst import run_algorithm
from catalyst.exchange.exchange_utils import get_exchange_symbols
from catalyst.api import (
symbols,
)
def initialize(context):
context.i = -1
context.base_currency = 'btc'
def handle_data(context, data):
lookback = 60 * 24 * 7 # (minutes, hours, days)
context.i += 1
if context.i < lookback:
return
today = context.blotter.current_dt.strftime('%Y-%m-%d %H:%M:%S')
try:
# update universe everyday
new_day = 60 * 24
if not context.i % new_day:
context.universe = universe(context, today)
# get data every 30 minutes
minutes = 30
if not context.i % minutes and context.universe:
for coin in context.coins:
pair = str(coin.symbol)
# ohlcv data
open = data.history(coin, 'open', lookback,
'1m').ffill().bfill().resample(
'30T').first()
high = data.history(coin, 'high', lookback,
'1m').ffill().bfill().resample('30T').max()
low = data.history(coin, 'low', lookback,
'1m').ffill().bfill().resample('30T').min()
close = data.history(coin, 'price', lookback,
'1m').ffill().bfill().resample(
'30T').last()
volume = data.history(coin, 'volume', lookback,
'1m').ffill().bfill().resample(
'30T').sum()
print(today, pair, close[-1])
except Exception as e:
print(e)
def analyze(context=None, results=None):
pass
def universe(context, today):
json_symbols = get_exchange_symbols('poloniex')
poloniex_universe_df = pd.DataFrame.from_dict(
json_symbols).transpose().astype(str)
poloniex_universe_df['base_currency'] = poloniex_universe_df.apply(
lambda row: row.symbol.split('_')[1],
axis=1)
poloniex_universe_df['market_currency'] = poloniex_universe_df.apply(
lambda row: row.symbol.split('_')[0],
axis=1)
poloniex_universe_df = poloniex_universe_df[
poloniex_universe_df['base_currency'] == context.base_currency]
poloniex_universe_df = poloniex_universe_df[
poloniex_universe_df.symbol != 'gas_btc']
# Markets currently not working on Catalyst 0.3.1
# 2017-01-01
# poloniex_universe_df = poloniex_universe_df[poloniex_universe_df.symbol != 'bcn_btc']
# poloniex_universe_df = poloniex_universe_df[poloniex_universe_df.symbol != 'burst_btc']
# poloniex_universe_df = poloniex_universe_df[poloniex_universe_df.symbol != 'dgb_btc']
# poloniex_universe_df = poloniex_universe_df[poloniex_universe_df.symbol != 'doge_btc']
# poloniex_universe_df = poloniex_universe_df[poloniex_universe_df.symbol != 'emc2_btc']
# poloniex_universe_df = poloniex_universe_df[poloniex_universe_df.symbol != 'pink_btc']
# poloniex_universe_df = poloniex_universe_df[poloniex_universe_df.symbol != 'sc_btc']
print(poloniex_universe_df.head())
date = str(today).split(' ')[0]
poloniex_universe_df = poloniex_universe_df[
poloniex_universe_df.start_date < date]
context.coins = symbols(*poloniex_universe_df.symbol)
print(len(poloniex_universe_df))
return poloniex_universe_df.symbol.tolist()
if __name__ == '__main__':
start_date = pd.to_datetime('2017-01-01', utc=True)
end_date = pd.to_datetime('2017-10-15', utc=True)
performance = run_algorithm(start=start_date, end=end_date,
capital_base=10000.0,
initialize=initialize,
handle_data=handle_data,
analyze=analyze,
exchange_name='poloniex',
data_frequency='minute',
base_currency='btc',
live=False,
live_graph=False,
algo_namespace='test')
+140
View File
@@ -0,0 +1,140 @@
"""
Requires Catalyst version 0.3.0 or above
Tested on Catalyst version 0.3.2
These example aims to provide and easy way for users to learn how to collect data from the different exchanges.
You simply need to specify the exchange and the market that you want to focus on.
You will all see how to create a universe and filter it base on the exchange and the market you desire.
The example prints out the closing price of all the pairs for a given market-exchange every 30 minutes.
The example also contains the ohlcv minute data for the past seven days which could be used to create indicators
Use this as the backbone to create your own trading strategies.
Variables lookback date and date are used to ensure data for a coin existed on the lookback period specified.
"""
import numpy as np
import pandas as pd
from datetime import timedelta
from catalyst import run_algorithm
from catalyst.exchange.exchange_utils import get_exchange_symbols
from catalyst.api import (
symbols,
)
def initialize(context):
context.i = -1 # counts the minutes
context.exchange = 'poloniex' # must match the exchange specified in run_algorithm
context.base_currency = 'eth' # must match the base currency specified in run_algorithm
def handle_data(context, data):
lookback = 60 * 24 * 7 # (minutes, hours, days) of how far to lookback in the data history
context.i += 1
# current date formatted into a string
today = context.blotter.current_dt
date, time = today.strftime('%Y-%m-%d %H:%M:%S').split(' ')
lookback_date = today - timedelta(days=(
lookback / (60 * 24))) # subtract the amount of days specified in lookback
lookback_date = lookback_date.strftime('%Y-%m-%d %H:%M:%S').split(' ')[
0] # get only the date as a string
# update universe everyday
new_day = 60 * 24
if not context.i % new_day:
context.universe = universe(context, lookback_date, date)
# get data every 30 minutes
minutes = 30
if not context.i % minutes and context.universe:
# we iterate for every pair in the current universe
for coin in context.coins:
pair = str(coin.symbol)
# 30 minute interval ohlcv data (the standard data required for candlestick or indicators/signals)
# 30T means 30 minutes re-sampling of one minute data. change to your desire time interval.
open = fill(data.history(coin, 'open', bar_count=lookback,
frequency='1m')).resample('30T').first()
high = fill(data.history(coin, 'high', bar_count=lookback,
frequency='1m')).resample('30T').max()
low = fill(data.history(coin, 'low', bar_count=lookback,
frequency='1m')).resample('30T').min()
close = fill(data.history(coin, 'price', bar_count=lookback,
frequency='1m')).resample('30T').last()
volume = fill(data.history(coin, 'volume', bar_count=lookback,
frequency='1m')).resample('30T').sum()
# close[-1] is the equivalent to current price
# displays the minute price for each pair every 30 minutes
print(
today, pair, open[-1], high[-1], low[-1], close[-1], volume[-1])
# ----------------------------------------------------------------------------------------------------------
# -------------------------------------- Insert Your Strategy Here -----------------------------------------
# ----------------------------------------------------------------------------------------------------------
def analyze(context=None, results=None):
pass
# Get the universe for a given exchange and a given base_currency market
# Example: Poloniex BTC Market
def universe(context, lookback_date, current_date):
json_symbols = get_exchange_symbols(
context.exchange) # get all the pairs for the exchange
universe_df = pd.DataFrame.from_dict(json_symbols).transpose().astype(
str) # convert into a dataframe
universe_df['base_currency'] = universe_df.apply(
lambda row: row.symbol.split('_')[1],
axis=1)
universe_df['market_currency'] = universe_df.apply(
lambda row: row.symbol.split('_')[0],
axis=1)
# Filter all the exchange pairs to only the ones for a give base currency
universe_df = universe_df[
universe_df['base_currency'] == context.base_currency]
# Filter all the pairs to ensure that pair existed in the current date range
universe_df = universe_df[universe_df.start_date < lookback_date]
universe_df = universe_df[universe_df.end_daily >= current_date]
context.coins = symbols(
*universe_df.symbol) # convert all the pairs to symbols
print(universe_df.head(), len(universe_df))
return universe_df.symbol.tolist()
# Replace all NA, NAN or infinite values with its nearest value
def fill(series):
if isinstance(series, pd.Series):
return series.replace([np.inf, -np.inf], np.nan).ffill().bfill()
elif isinstance(series, np.ndarray):
return pd.Series(series).replace([np.inf, -np.inf],
np.nan).ffill().bfill().values
else:
return series
if __name__ == '__main__':
start_date = pd.to_datetime('2017-01-01', utc=True)
end_date = pd.to_datetime('2017-10-15', utc=True)
performance = run_algorithm(start=start_date, end=end_date,
capital_base=10000.0,
initialize=initialize,
handle_data=handle_data,
analyze=analyze,
exchange_name='poloniex',
data_frequency='minute',
base_currency='eth',
live=False,
live_graph=False,
algo_namespace='simple_universe')
"""
Run in Terminal (inside catalyst environment):
python simple_universe.py
"""
+153
View File
@@ -0,0 +1,153 @@
import pandas as pd
from logbook import Logger, DEBUG
from catalyst import run_algorithm
from catalyst.api import (schedule_function, order_target_percent, symbol,
date_rules, get_open_orders, cancel_order, record,
set_commission, set_slippage)
log = Logger('rodrigo_1', level=DEBUG)
"""
The initialize function sets any data or variables that
you'll use in your algorithm.
It's only called once at the beginning of your algorithm.
"""
def initialize(context):
# Select asset of interest
context.asset = symbol('BTC_USD')
# set_commission(TradingPairFeeSchedule(maker_fee=0.5, taker_fee=0.5))
# set_slippage(TradingPairFixedSlippage(spread=0.5))
# Set up a rebalance method to run every day
schedule_function(rebalance, date_rule=date_rules.every_day())
"""
Rebalance function scheduled to run once per day.
"""
def rebalance(context, data):
# To make market decisions, we're calculating the token's
# moving average for the last 5 days.
# We get the price history for the last 5 days.
price_history = data.history(context.asset, fields='price', bar_count=5,
frequency='1d')
# Then we take an average of those 5 days.
average_price = price_history.mean()
# We also get the coin's current price.
price = data.current(context.asset, 'price')
# Cancel any outstanding orders
orders = get_open_orders(context.asset) or []
for order in orders:
cancel_order(order)
# If our coin is currently listed on a major exchange
if data.can_trade(context.asset):
# If the current price is 1% above the 5-day average price,
# we open a long position. If the current price is below the
# average price, then we want to close our position to 0 shares.
if price > (1.01 * average_price):
# Place the buy order (positive means buy, negative means sell)
order_target_percent(context.asset, .99)
log.info("Buying %s" % (context.asset.symbol))
elif price < average_price:
# Sell all of our shares by setting the target position to zero
order_target_percent(context.asset, 0)
log.info("Selling %s" % (context.asset.symbol))
# Use the record() method to track up to five custom signals.
# Record Apple's current price and the average price over the last
# five days.
cash = context.portfolio.cash
leverage = context.account.leverage
record(price=price, average_price=average_price, cash=cash,
leverage=leverage)
def analyze(context=None, results=None):
import matplotlib.pyplot as plt
# Plot the portfolio and asset data.
ax1 = plt.subplot(511)
results[['portfolio_value']].plot(ax=ax1)
ax1.set_ylabel('Portfolio Value (USD)')
ax2 = plt.subplot(512, sharex=ax1)
ax2.set_ylabel('{asset} (USD)'.format(asset=context.asset))
(results[[
'price',
]]).plot(ax=ax2)
trans = results.ix[[t != [] for t in results.transactions]]
buys = trans.ix[
[t[0]['amount'] > 0 for t in trans.transactions]
]
sells = trans.ix[
[t[0]['amount'] < 0 for t in trans.transactions]
]
ax2.plot(
buys.index,
results.price[buys.index],
'^',
markersize=10,
color='g',
)
ax2.plot(
sells.index,
results.price[sells.index],
'v',
markersize=10,
color='r',
)
ax3 = plt.subplot(513, sharex=ax1)
results[['leverage']].plot(ax=ax3)
ax3.set_ylabel('Leverage ')
ax4 = plt.subplot(514, sharex=ax1)
results[['cash']].plot(ax=ax4)
ax4.set_ylabel('Cash (USD)')
results[[
'algorithm',
'benchmark',
]] = results[[
'algorithm_period_return',
'benchmark_period_return',
]]
ax5 = plt.subplot(515, sharex=ax1)
results[[
'algorithm',
'benchmark',
]].plot(ax=ax5)
ax5.set_ylabel('Percent Change')
plt.legend(loc=3)
# Show the plot.
plt.gcf().set_size_inches(18, 8)
plt.show()
run_algorithm(
capital_base=100000,
start=pd.to_datetime('2017-1-1', utc=True),
end=pd.to_datetime('2017-10-22', utc=True),
data_frequency='minute',
initialize=initialize,
handle_data=None,
analyze=analyze,
exchange_name='bitfinex',
algo_namespace='rodrigo_1',
base_currency='usd'
)
+1 -1
View File
@@ -31,7 +31,7 @@ import catalyst.utils.paths as pth
from catalyst.exchange.exchange_algorithm import ExchangeTradingAlgorithmLive, \
ExchangeTradingAlgorithmBacktest
from catalyst.exchange.data_portal_exchange import DataPortalExchangeLive, \
from catalyst.exchange.exchange_data_portal import DataPortalExchangeLive, \
DataPortalExchangeBacktest
from catalyst.exchange.asset_finder_exchange import AssetFinderExchange
from catalyst.exchange.exchange_portfolio import ExchangePortfolio
-105
View File
@@ -1,105 +0,0 @@
<h1>Live Trading</h1>
This document explains how to get started with live trading.
<h2>Supported Exchanges</h2>
Catalyst can trade against these exchanges:
* Bitfinex, id=`bitfinex`
* Bittrex, id=`bittrex`
<h3>Authentication</h3>
Most exchanges require key/token combination for authentication. By
convention, Catalyst uses an "auth.json" file to hold this data.
This example illustrates the convention using the Bitfinex exchange.
Here is how to generate key and secret values for bitfinex:
https://docs.bitfinex.com/v1/docs/api-access. Most exchanges follow
a similar process.
The auth.json file:
```json
{
"name": "bitfinex",
"key": "my-key",
"secret": "my-secret"
}
```
The file goes here:
```
~/.catalyst/data/exchanges/bitfinex/auth.json
```
Note that the 'bitfinex' directory corresponds to the id of the Bitfinex
exchange as defined in the "Supported Exchanges" section above.
Attempting to run an algorithm where the targeted exchange is missing
its "auth.json" file will create the directory structure but result
in an error.
<h3>Currency Symbols</h3>
Catalyst introduces a universal convention to reference
trading pairs and individual currencies. This
is required to ensure that the `symbol()` api predictably
returns the correct asset regardless of the targeted exchange.
Exchanges tend to use their own convention to represent currencies
(e.g. XBT and BTC both represent Bitcoin on different exchanges).
Trading pairs are also inconsistent. For example, Bitfinex
puts the market currency before the base currency without a
separator, Bittrex puts the base currency first and uses a dash
seperator.
Here is the Catalyst convention:
*[Market Currency]_[Base Currency]* all lowercase.
Currency symbols (e.g. btc, eth, ltc) follow the Bittrex convention.
Here are some examples:
```python
# With Bitfinex
bitcoin_usd_asset = symbol('btc_usd')
ethereum_bitcoin_asset = symbol('eth_btc')
# With Bittrex
ethereum_bitcoin_asset = symbol('eth_btc')
neo_ethereum_asset = symbol('neo_eth)
```
Note that the trading pairs are always referenced in the same manner.
However, not all trading pairs are available on all exchanges. An
error will occur if the specified trading pair is not trading
on the exchange.
<h2>Trading an Algorithm</h2>
There is no special convention to follow when writing an
algorithm for live trading. The same algorithm should work in
backtest and live execution mode without modification.
What differs are the arguments provided to the catalyst client or
`run_algorithm()` interface. Here is example:
```python
run_algorithm(
initialize=initialize,
handle_data=handle_data,
analyze=analyze,
exchange_name='bitfinex',
live=True,
algo_namespace='my_algo_trading_xrp',
base_currency='btc'
)
```
Here is the breakdown of the new arguments:
* live: Boolean flag which enables live trading.
* exchange_name: The name of the targeted exchange
(supported values: *bitfinex*, *bittrex*).
* algo_namespace: A arbitrary label assigned to your algorithm for
data storage purposes.
* base_currency: The base currency used to calculate the
statistics of your algorithm. Currently, the base currency of all
trading pairs of your algorithm must match this value.
Here is a complete algorithm for reference:
[Buy Low and Sell High](../catalyst/examples/buy_low_sell_high_live.py)
+5 -1
View File
@@ -255,7 +255,7 @@ slippage model that ``catalyst`` uses).
Let's take a quick look at the performance ``DataFrame``. For this, we
use ``pandas`` from inside the IPython Notebook and print the first ten
rows. and print the first ten rows. Note that ``catalyst`` makes heavy usage of
rows. Note that ``catalyst`` makes heavy usage of
`pandas <http://pandas.pydata.org/>`_, especially for data input and
outputting so it's worth spending some time to learn it.
@@ -486,6 +486,10 @@ collect, the second argument is the unit (either ``'1d'`` for ``'1m'``
but note that you need to have minute-level data for using ``1m``). This is
a function we use in the ``handle_data()`` section:
.. code-block:: python
%load_ext catalyst
.. code-block:: python
%%catalyst --start 2016-4-1 --end 2017-9-30 -x bitfinex
+30 -105
View File
@@ -1,21 +1,17 @@
Development Guidelines
======================
This page is intended for developers of Zipline, people who want to contribute to the Zipline codebase or documentation, or people who want to install from source and make local changes to their copy of Zipline.
This page is intended for developers of Catalyst, people who want to contribute to the Catalyst codebase or documentation, or people who want to install from source and make local changes to their copy of Catalyst.
All contributions, bug reports, bug fixes, documentation improvements, enhancements and ideas are welcome. We `track issues`__ on `GitHub`__ and also have a `mailing list`__ where you can ask questions.
__ https://github.com/quantopian/zipline/issues
__ https://github.com/
__ https://groups.google.com/forum/#!forum/zipline
All contributions, bug reports, bug fixes, documentation improvements, enhancements and ideas are welcome. We `track issues <https://github.com/enigmampc/catalyst/issues>`_ on `GitHub <https://github.com/enigmampc/catalyst>`_ and also have a `discord group <https://discord.gg/SJK32GY>`_ where you can ask questions.
Creating a Development Environment
----------------------------------
First, you'll need to clone Zipline by running:
First, you'll need to clone Catalyst by running:
.. code-block:: bash
$ git clone git@github.com:your-github-username/zipline.git
$ git clone git@github.com:enigmampc/catalyst.git
Then check out to a new branch where you can make your changes:
@@ -23,15 +19,13 @@ Then check out to a new branch where you can make your changes:
$ git checkout -b some-short-descriptive-name
If you don't already have them, you'll need some C library dependencies. You can follow the `install guide`__ to get the appropriate dependencies.
__ install.html
If you don't already have them, you'll need some C library dependencies. You can follow the `install guide <install.html>`_ to get the appropriate dependencies.
The following section assumes you already have virtualenvwrapper and pip installed on your system. Suggested installation of Python library dependencies used for development:
.. code-block:: bash
$ mkvirtualenv zipline
$ mkvirtualenv catalyst
$ ./etc/ordered_pip.sh ./etc/requirements.txt
$ pip install -r ./etc/requirements_dev.txt
$ pip install -r ./etc/requirements_blaze.txt
@@ -42,104 +36,39 @@ Finally, you can build the C extensions by running:
$ python setup.py build_ext --inplace
To finish, make sure `tests`__ pass.
.. To finish, make sure `tests`__ pass.
__ #style-guide-running-tests
.. __ #style-guide-running-tests
If you get an error running nosetests after setting up a fresh virtualenv, please try running
.. If you get an error running nosetests after setting up a fresh virtualenv, please try running
.. code-block:: bash
.. code-block
# where zipline is the name of your virtualenv
$ deactivate zipline
$ workon zipline
.. # where zipline is the name of your virtualenv
.. $ deactivate zipline
.. $ workon zipline
Development with Docker
.. Development with Docker
.. -----------------------
..If you want to work with zipline using a `Docker`__ container, you'll need to build the ``Dockerfile`` in the Zipline root directory, and then build ``Dockerfile-dev``. Instructions for building both containers can be found in ``Dockerfile`` and ``Dockerfile-dev``, respectively.
.. __ https://docs.docker.com/get-started/
Git Branching Structure
-----------------------
If you want to work with zipline using a `Docker`__ container, you'll need to build the ``Dockerfile`` in the Zipline root directory, and then build ``Dockerfile-dev``. Instructions for building both containers can be found in ``Dockerfile`` and ``Dockerfile-dev``, respectively.
If you want to contribute to the codebase of Catalyst, familiarize yourself with our branching structure, a fairly standardized one for that matter, that follows what is documented in the following article: `A successful Git branching model <http://nvie.com/posts/a-successful-git-branching-model/>`_. To contribute, create your local branch and submit a Pull Request (PR) to the **develop** branch.
__ https://docs.docker.com/get-started/
.. image:: https://camo.githubusercontent.com/9bde6fb64a9542a572e0e2017cbb58d9d2c440ac/687474703a2f2f6e7669652e636f6d2f696d672f6769742d6d6f64656c4032782e706e67
Style Guide & Running Tests
---------------------------
We use `flake8`__ for checking style requirements and `nosetests`__ to run Zipline tests. Our `continuous integration`__ tools will run these commands.
__ http://flake8.pycqa.org/en/latest/
__ http://nose.readthedocs.io/en/latest/
__ https://en.wikipedia.org/wiki/Continuous_integration
Before submitting patches or pull requests, please ensure that your changes pass when running:
.. code-block:: bash
$ flake8 zipline tests
In order to run tests locally, you'll need `TA-lib`__, which you can install on Linux by running:
__ https://mrjbq7.github.io/ta-lib/install.html
.. code-block:: bash
$ wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz
$ tar -xvzf ta-lib-0.4.0-src.tar.gz
$ cd ta-lib/
$ ./configure --prefix=/usr
$ make
$ sudo make install
And for ``TA-lib`` on OS X you can just run:
.. code-block:: bash
$ brew install ta-lib
Then run ``pip install`` TA-lib:
.. code-block:: bash
$ pip install -r ./etc/requirements_talib.txt
You should now be free to run tests:
.. code-block:: bash
$ nosetests
Continuous Integration
----------------------
We use `Travis CI`__ for Linux-64 bit builds and `AppVeyor`__ for Windows-64 bit builds.
.. note::
We do not currently have CI for OSX-64 bit builds. 32-bit builds may work but are not included in our integration tests.
__ https://travis-ci.org/quantopian/zipline
__ https://ci.appveyor.com/project/quantopian/zipline
Packaging
---------
To learn about how we build Zipline conda packages, you can read `this`__ section in our release process notes.
__ release-process.html#uploading-conda-packages
Contributing to the Docs
------------------------
If you'd like to contribute to the documentation on zipline.io, you can navigate to ``docs/source/`` where each `reStructuredText`__ (``.rst``) file is a separate section there. To add a section, create a new file called ``some-descriptive-name.rst`` and add ``some-descriptive-name`` to ``appendix.rst``. To edit a section, simply open up one of the existing files, make your changes, and save them.
__ https://en.wikipedia.org/wiki/ReStructuredText
We use `Sphinx`__ to generate documentation for Zipline, which you will need to install by running:
__ http://www.sphinx-doc.org/en/stable/
If you'd like to contribute to the documentation on enigmampc.github.io, you can navigate to ``docs/source/`` where each `reStructuredText <https://en.wikipedia.org/wiki/ReStructuredText>`_ file is a separate section there. To add a section, create a new file called ``some-descriptive-name.rst`` and add ``some-descriptive-name`` to ``index.rst``. To edit a section, simply open up one of the existing files, make your changes, and save them.
We use `Sphinx <http://www.sphinx-doc.org/en/stable/>`_ to generate documentation for Catalyst, which you will need to install by running:
.. code-block:: bash
@@ -149,7 +78,7 @@ To build and view the docs locally, run:
.. code-block:: bash
# assuming you're in the Zipline root directory
# assuming you're in the Catalyst root directory
$ cd docs
$ make html
$ {BROWSER} build/html/index.html
@@ -162,7 +91,7 @@ Standard prefixes to start a commit message:
.. code-block:: text
BLD: change related to building Zipline
BLD: change related to building Catalyst
BUG: bug fix
DEP: deprecate something, or remove a deprecated object
DEV: development tool or utility
@@ -172,15 +101,13 @@ Standard prefixes to start a commit message:
REV: revert an earlier commit
STY: style fix (whitespace, PEP8, flake8, etc)
TST: addition or modification of tests
REL: related to releasing Zipline
REL: related to releasing Catalyst
PERF: performance enhancements
Some commit style guidelines:
Commit lines should be no longer than `72 characters`__. The first line of the commit should include one of the above prefixes. There should be an empty line between the commit subject and the body of the commit. In general, the message should be in the imperative tense. Best practice is to include not only what the change is, but why the change was made.
__ https://git-scm.com/book/en/v2/Distributed-Git-Contributing-to-a-Project
Commit lines should be no longer than `72 characters <https://git-scm.com/book/en/v2/Distributed-Git-Contributing-to-a-Project>`_. The first line of the commit should include one of the above prefixes. There should be an empty line between the commit subject and the body of the commit. In general, the message should be in the imperative tense. Best practice is to include not only what the change is, but why the change was made.
**Example:**
@@ -203,8 +130,6 @@ __ https://git-scm.com/book/en/v2/Distributed-Git-Contributing-to-a-Project
Formatting Docstrings
---------------------
When adding or editing docstrings for classes, functions, etc, we use `numpy`__ as the canonical reference.
__ https://github.com/numpy/numpy/blob/master/doc/HOWTO_DOCUMENT.rst.txt
When adding or editing docstrings for classes, functions, etc, we use `numpy <https://github.com/numpy/numpy/blob/master/doc/HOWTO_DOCUMENT.rst.txt>`_ as the canonical reference.
+7 -1
View File
@@ -9,9 +9,15 @@ Table of Contents
install
beginner-tutorial
jupyter
live-trading
naming-convention
videos
resources
development-guidelines
releases
.. bundles
.. development-guidelines
.. appendix
.. release-process
.. releases
+130 -74
View File
@@ -1,6 +1,13 @@
Install
=======
To get started with Catalyst, you will need to install it in your computer.
Like any other piece of software, Catalyst has a number of dependencies
(other software on which it depends to run) that you will need to install, as
well. We recommend using a software named ``Conda`` that will manage all
these dependencies for you, and set up the environment needed to get you up
and running as easily as possible. See :ref:`Installing with Conda <conda>`.
Installing with ``pip``
-----------------------
@@ -9,19 +16,20 @@ Python package.
There are two reasons for the additional complexity:
1. Catalyst ships several C extensions that require access to the CPython C API.
In order to build the C extensions, ``pip`` needs access to the CPython
header files for your Python installation.
1. Catalyst ships several C extensions that require access to the CPython C
API. In order to build the C extensions, ``pip`` needs access to the
CPython header files for your Python installation.
2. Catalyst depends on `numpy <http://www.numpy.org/>`_, the core library for
numerical array computing in Python. Numpy depends on having the `LAPACK
<http://www.netlib.org/lapack>`_ linear algebra routines available.
Because LAPACK and the CPython headers are non-Python dependencies, the correct
way to install them varies from platform to platform. If you'd rather use a
single tool to install Python and non-Python dependencies, or if you're already
using `Anaconda <http://continuum.io/downloads>`_ as your Python distribution,
you can skip to the :ref:`Installing with Conda <conda>` section.
Because LAPACK and the CPython headers are non-Python dependencies, the
correctway to install them varies from platform to platform. If you'd rather
use a single tool to install Python and non-Python dependencies, or if you're
already using `Anaconda <http://continuum.io/downloads>`_ as your Python
distribution, you can skip to the :ref:`Installing with Conda <conda>`
section.
Once you've installed the necessary additional dependencies (see below for
your particular platform), you should be able to simply run
@@ -34,8 +42,8 @@ If you use Python for anything other than Catalyst, we **strongly** recommend
that you install in a `virtualenv
<https://virtualenv.readthedocs.org/en/latest>`_. The `Hitchhiker's Guide to
Python`_ provides an `excellent tutorial on virtualenv
<http://docs.python-guide.org/en/latest/dev/virtualenvs/>`_. Here's a summarized
version:
<http://docs.python-guide.org/en/latest/dev/virtualenvs/>`_. Here's a
summarized version:
.. code-block:: bash
@@ -44,9 +52,10 @@ version:
$ source ./catalyst-venv/bin/activate
$ pip install enigma-catalyst
Though not required by Catalyst directly, our example algorithms use matplotlib
to visually display the results of the trading algorithms. If you wish to run
any examples or use matplotlib during development, it can be installed using:
Though not required by Catalyst directly, our example algorithms use
matplotlib to visually display the results of the trading algorithms. If you
wish to run any examples or use matplotlib during development, it can be
installed using:
.. code-block:: bash
@@ -91,12 +100,12 @@ On `Arch Linux`_, you can acquire the additional dependencies via ``pacman``:
OSX
~~~
The version of Python shipped with OSX by default is generally out of date, and
has a number of quirks because it's used directly by the operating system. For
these reasons, many developers choose to install and use a separate Python
The version of Python shipped with OSX by default is generally out of date,
and has a number of quirks because it's used directly by the operating system.
For these reasons, many developers choose to install and use a separate Python
installation. The `Hitchhiker's Guide to Python`_ provides an excellent guide
to `Installing Python on OSX <http://docs.python-guide.org/en/latest/>`_, which
explains how to install Python with the `Homebrew`_ manager.
to `Installing Python on OSX <http://docs.python-guide.org/en/latest/>`_,
which explains how to install Python with the `Homebrew`_ manager.
Assuming you've installed Python with Homebrew, you'll also likely need the
following brew packages:
@@ -108,58 +117,85 @@ following brew packages:
OSX + virtualenv + matplotlib
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
A note about using matplotlib in virtual enviroments on OSX: it may be necessary to run
A note about using matplotlib in virtual enviroments on OSX: it may be
necessary to run
.. code-block:: bash
echo "backend: TkAgg" > ~/.matplotlib/matplotlibrc
in order to override the default ``macosx`` backend for your system, which may not
be accessible from inside the virtual environment. This will allow Catalyst to open
matplotlib charts from within a virtual environment, which is useful for displaying
the performance of your backtests. To learn more about matplotlib backends, please refer to the
in order to override the default ``macosx`` backend for your system, which
may not be accessible from inside the virtual environment. This will allow
Catalyst to open matplotlib charts from within a virtual environment, which
is useful for displaying the performance of your backtests. To learn more
about matplotlib backends, please refer to the
`matplotlib backend documentation <https://matplotlib.org/faq/usage_faq.html#what-is-a-backend>`_.
.. _windows:
Windows
~~~~~~~
In Windows, you will need the `Microsoft Visual C++ Compiler for Python 2.7
<https://www.microsoft.com/en-us/download/details.aspx?id=44266>`_. This package
contains the compiler and the set of system headers necessary for producing
binary wheels for Python 2.7 packages. If it's not already in your system, download
it and install it before proceeding to the next step.
<https://www.microsoft.com/en-us/download/details.aspx?id=44266>`_. This
package contains the compiler and the set of system headers necessary for
producing binary wheels for Python 2.7 packages. If it's not already in your
system, download it and install it before proceeding to the next step.
For windows, the easiest and best supported way to install Catalyst is to use
:ref:`Conda <conda>`.
Troubleshooting Visual C++ Compiler Install
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Some problems we have encountered installing the **Visual C++ Compiler**
mentioned above are as follows:
We run into two different errors when trying to install the the `Microsoft Visual C++
Compiler for Python 2.7` mentioned above:
- **The system administrator has set policies to prevent this installation**.
In some systems, there is a default *Windows Software Restriction* policy
that prevents the installation of some software packages like this one.
You'll have to change the Registry to circumvent this:
-
- Click ``Start``, and search for ``regedit`` and launch the
``Registry Editor``
- Navigate to the following folder:
``HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\Installer``
- If there is an entry for ``DisableMSI``, set the Value data to 0.
- If there is no such entry, click on the ``Edit`` menu -> ``New`` ->
``DWORD (32-bit) Value`` and enter ``DisableMSI`` as the Name (and by
default you get 0 as the Value Data)
|
- **The installer has encountered an unexpected error installing this package.
This may indicate a problem with this package. The error code is 2503.**
We have observed this when trying to install a package without enough
administrator permissions. Even when you are logged in as an Administrator,
you have to explictily install this package with administrator privileges:
- Click ``Start`` and find ``CMD`` or ``Command Prompt``
- Right click on it and choose ``Run as administrator``
- ``cd`` into the folder where you downloaded ``VCForPython27.msi``
- Run ``msiexec /i VCForPython27.msi``
Amazon Linux AMI
~~~~~~~~~~~~~~~~
The packages ``pip`` and ``setuptools`` that come shipped by default are very outdated.
Thus, you first need to run:
The packages ``pip`` and ``setuptools`` that come shipped by default are very
outdated. Thus, you first need to run:
.. code-block:: bash
pip install --upgrade pip setuptools
The default installation is also missing the C and C++ compilers, which you install by:
The default installation is also missing the C and C++ compilers, which you
install by:
.. code-block:: bash
sudo yum install gcc gcc-c++
Then you should follow the regular installation instructions outlined at the beginning
of this page.
Then you should follow the regular installation instructions outlined at the
beginning of this page.
Troubleshooting ``pip`` Install
@@ -184,17 +220,24 @@ Troubleshooting ``pip`` Install
----
**Issue**:
Package enigma-catalyst cannot still be found, even after upgrading pip (see above), with an error similar to:
Package enigma-catalyst cannot still be found, even after upgrading pip
(see above), with an error similar to:
.. code-block:: bash
Downloading/unpacking enigma-catalyst
Could not find a version that satisfies the requirement enigma-catalyst (from versions: 0.1.dev9, 0.2.dev2, 0.1.dev4, 0.1.dev5, 0.1.dev3, 0.2.dev1, 0.1.dev8, 0.1.dev6)
Could not find a version that satisfies the requirement enigma-catalyst
(from versions: 0.1.dev9, 0.2.dev2, 0.1.dev4, 0.1.dev5, 0.1.dev3,
0.2.dev1, 0.1.dev8, 0.1.dev6)
Cleaning up...
No distributions matching the version for enigma-catalyst
**Solution**:
In some systems (this error has been reported in Ubuntu), pip is configured to only find stable versions by default. Since Catalyst is in alpha version, pip cannot find a matching version that satisfies the installation requirements. The solution is to include the `--pre` flag to include pre-release and development versions:
In some systems (this error has been reported in Ubuntu), pip is configured
to only find stable versions by default. Since Catalyst is in alpha
version, pip cannot find a matching version that satisfies the installation
requirements. The solution is to include the `--pre` flag to include
pre-release and development versions:
.. code-block:: bash
@@ -230,10 +273,14 @@ Troubleshooting ``pip`` Install
----
**Issue**:
Installation fails with error: ``fatal error: Python.h: No such file or directory``
Installation fails with error:
``fatal error: Python.h: No such file or directory``
**Solution**:
Some systems (this issue has been reported in Ubuntu) require `python-dev` for the proper build and installation of package dependencies. The solution is to install python-dev, which is independent of the virtual environment. In Ubuntu, you would need to run:
Some systems (this issue has been reported in Ubuntu) require `python-dev`
for the proper build and installation of package dependencies. The solution
is to install python-dev, which is independent of the virtual environment.
In Ubuntu, you would need to run:
.. code-block:: bash
@@ -251,36 +298,41 @@ comes as part of Continuum Analytics' `Anaconda
The primary advantage of using Conda over ``pip`` is that conda natively
understands the complex binary dependencies of packages like ``numpy`` and
``scipy``. This means that ``conda`` can install Catalyst and its dependencies
without requiring the use of a second tool to acquire Catalyst's non-Python
dependencies.
``scipy``. This means that ``conda`` can install Catalyst and its
dependencies without requiring the use of a second tool to acquire Catalyst's
non-Python dependencies.
For Windows, you will need the *Microsoft Visual C++ Compiler for Python
2.7*. Follow the instructions on the :ref:`Windows` section and come back
here.
For instructions on how to install ``conda``, see the `Conda Installation
Documentation <http://conda.pydata.org/docs/download.html>`_. Alternatively, you
can install MiniConda, which is a smaller footprint (fewer packages and smaller
size) than its big brother Anaconda, but it still contains all the main packages
needed. To install MiniConda, you can follow these steps:
Documentation <http://conda.pydata.org/docs/download.html>`_. Alternatively,
you can install MiniConda, which is a smaller footprint (fewer packages and
smaller size) than its big brother Anaconda, but it still contains all the
main packages needed. To install MiniConda, you can follow these steps:
1. Download `MiniConda <https://conda.io/miniconda.html>`_. Select Python 2.7 for
your Operating System.
2. Install MiniConda. See the `Installation Instructions <https://conda.io/docs/user-guide/install/index.html>`_
if you need help.
3. Ensure the correct installation by running ``conda list`` in a Terminal window,
which should print the list of packages installed with Conda.
1. Download `MiniConda <https://conda.io/miniconda.html>`_. Select Python 2.7
for your Operating System.
2. Install MiniConda. See the `Installation Instructions
<https://conda.io/docs/user-guide/install/index.html>`_ if you need help.
3. Ensure the correct installation by running ``conda list`` in a Terminal
window, which should print the list of packages installed with Conda.
Once either Conda or MiniConda has been set up you can install Catalyst:
1. Download the file `python2.7-environment.yml <https://github.com/enigmampc/catalyst/blob/master/etc/python2.7-environment.yml>`_.
2. Open a Terminal window and enter [``cd/dir``] into the directory where you saved
the above ``python2.7-environment.yml`` file.
1. Download the file `python2.7-environment.yml
<https://github.com/enigmampc/catalyst/blob/master/etc/python2.7-environment.yml>`_.
2. Open a Terminal window and enter [``cd/dir``] into the directory where you
saved the above ``python2.7-environment.yml`` file.
3. Install using this file. This step can take about 5-10 minutes to install.
.. code-block:: bash
conda env create -f python2.7-environment.yml
4. Activate the environment (which you need to do every time you start a new session
to run Catalyst):
4. Activate the environment (which you need to do every time you start a new
session to run Catalyst):
**Linux or OSX:**
@@ -299,14 +351,15 @@ Congratulations! You now have Catalyst installed.
Troubleshooting ``conda`` Install
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If the command ``conda env create -f python2.7-environment.yml`` in step 3 above failed
for any reason, you can try setting up the environment manually with the following steps:
If the command ``conda env create -f python2.7-environment.yml`` in step 3
above failed for any reason, you can try setting up the environment manually
with the following steps:
1. Create the environment:
.. code-block:: bash
conda create --name catalyst python=2.7 scipy
conda create --name catalyst python=2.7 scipy zlib
2. Activate the environment:
@@ -331,20 +384,23 @@ for any reason, you can try setting up the environment manually with the followi
Getting Help
------------
If after following the instructions above, and going through the *Troubleshooting* sections,
you still experience problems installing Catalyst, you can seek additional help through the
following channels:
If after following the instructions above, and going through the
*Troubleshooting* sections, you still experience problems installing Catalyst,
you can seek additional help through the following channels:
- Join our `Discord community <https://discord.gg/SJK32GY>`_, and head over the #catalyst_dev
channel where many other users (as well as the project developers) hang out, and can assist
you with your particular issue. The more descriptive and the more information you can provide,
the easiest will be for others to help you out.
- Join our `Discord community <https://discord.gg/SJK32GY>`_, and head over
the #catalyst_dev channel where many other users (as well as the project
developers) hang out, and can assist you with your particular issue. The
more descriptive and the more information you can provide, the easiest will
be for others to help you out.
- Report the problem you are experiencing on our
`GitHub repository <https://github.com/enigmampc/catalyst/issues>`_ following the guidelines
provided therein. Before you do so, take a moment to browse through all `previous reported issues
<https://github.com/enigmampc/catalyst/issues?utf8=%E2%9C%93&q=is%3Aissue>`_ in the likely case
that someone else experienced that same issue before, and you get a hint on how to solve it.
`GitHub repository <https://github.com/enigmampc/catalyst/issues>`_
following the guidelines provided therein. Before you do so, take a moment
to browse through all `previous reported issues
<https://github.com/enigmampc/catalyst/issues?utf8=%E2%9C%93&q=is%3Aissue>`_
in the likely case that someone else experienced that same issue before,
and you get a hint on how to solve it.
.. _`Debian-derived`: https://www.debian.org/misc/children-distros
File diff suppressed because it is too large Load Diff
+118
View File
@@ -0,0 +1,118 @@
Live Trading
============
This document explains how to get started with live trading.
Supported Exchanges
^^^^^^^^^^^^^^^^^^^
Catalyst can trade against these exchanges:
- Bitfinex, id= ``bitfinex``
- Bittrex, id= ``bittrex``
- Poloniex, id= ``poloniex``
Authentication
^^^^^^^^^^^^^^
Most exchanges require token key/secret combination for authentication. By
convention, Catalyst uses an ``auth.json`` file to hold this data.
This example illustrates the convention using the *Bitfinex* exchange.
Here is how to generate key and secret values for the Bitfinex exchange:
https://docs.bitfinex.com/v1/docs/api-access. Most exchanges follow
a similar process.
The auth.json file:
.. code-block:: json
{
"name": "bitfinex",
"key": "my-key",
"secret": "my-secret"
}
The file goes here: ``~/.catalyst/data/exchanges/bitfinex/auth.json``
Note that the `bitfinex` part in the directory above corresponds to the id of the Bitfinex
exchange as defined in the "Supported Exchanges" section above.
Attempting to run an algorithm where the targeted exchange is missing
its ``auth.json`` file will create the directory structure and create an empty
auth.json file, but will result in an error.
Currency Symbols
^^^^^^^^^^^^^^^^
Catalyst introduces a universal convention to reference
trading pairs and individual currencies. This
is required to ensure that the ``symbol()`` api predictably
returns the correct asset regardless of the targeted exchange.
Exchanges tend to use their own convention to represent currencies
(e.g. XBT and BTC both represent Bitcoin on different exchanges).
Trading pairs are also inconsistent. For example, Bitfinex
puts the market currency before the base currency without a
separator, Bittrex puts the base currency first and uses a dash
seperator.
Here is the Catalyst convention:
*[Market Currency]_[Base Currency]* all lowercase.
Currency symbols (e.g. btc, eth, ltc) follow the Bittrex convention.
Here are some examples:
.. code-block:: json
# With Bitfinex
bitcoin_usd_asset = symbol('btc_usd')
ethereum_bitcoin_asset = symbol('eth_btc')
# With Bittrex
ethereum_bitcoin_asset = symbol('eth_btc')
neo_ethereum_asset = symbol('neo_eth)
Note that the trading pairs are always referenced in the same manner.
However, not all trading pairs are available on all exchanges. An
error will occur if the specified trading pair is not trading
on the exchange. To check which currency pairs are available on each
of the supported exchanges, see `Catalyst Market Coverage <https://www.enigma.co/catalyst/status`_.
Trading an Algorithm
^^^^^^^^^^^^^^^^^^^^
There is no special convention to follow when writing an
algorithm for live trading. The same algorithm should work in
backtest and live execution mode without modification.
What differs are the arguments provided to the catalyst client or
`run_algorithm()` interface. Here is the same example in both interfaces:
.. code-block:: bash
catalyst live -f my_algo_code -x bitfinex -c btc -n my_algo_name
.. code-block:: python
run_algorithm(
initialize=initialize,
handle_data=handle_data,
analyze=analyze,
exchange_name='bitfinex',
live=True,
algo_namespace='my_algo_name',
base_currency='btc'
)
Here is the breakdown of the new arguments:
- ``live``: Boolean flag which enables live trading.
- ``exchange_name``: The name of the targeted exchange
(supported values: *bitfinex*, *bittrex*).
- ``algo_namespace``: A arbitrary label assigned to your algorithm for
data storage purposes.
- ``base_currency``: The base currency used to calculate the
statistics of your algorithm. Currently, the base currency of all
trading pairs of your algorithm must match this value.
Here is a complete algorithm for reference:
`Buy Low and Sell High <https://github.com/enigmampc/catalyst/blob/master/catalyst/examples/buy_low_sell_high_live.py>`_
+150 -11
View File
@@ -2,24 +2,163 @@
Release Notes
=============
.. include:: whatsnew/1.1.1.txt
Version 0.3.4
^^^^^^^^^^^^^
**Release Date**: 2017-10-31
.. include:: whatsnew/1.1.0.txt
Bug Fixes
~~~~~~~~~
.. include:: whatsnew/1.0.2.txt
- Fixed issue with auto-ingestion of minute data
- Fixed issue with sell orders in backtesting
- Fixed data frequency issues with data.history() in backtesting
- Fixed an issue with can_trade()
.. include:: whatsnew/1.0.1.txt
.. include:: whatsnew/1.0.0.txt
Build
~~~~~
.. include:: whatsnew/0.9.0.txt
- Added more unit tests
.. include:: whatsnew/0.8.4.txt
Version 0.3.3
^^^^^^^^^^^^^
**Release Date**: 2017-10-26
.. include:: whatsnew/0.8.3.txt
Bug Fixes
~~~~~~~~~
.. include:: whatsnew/0.8.0.txt
- Fix missing -x in ingest-exchange
- Fix issue with daily chunks end date (data bundles)
- Fix issue in the prepare_chunk logic (data bundles)
.. include:: whatsnew/0.7.0.txt
Build
~~~~~
- Added data validation unit tests
Version 0.3.2
^^^^^^^^^^^^^
**Release Date**: 2017-10-25
Bug Fixes
~~~~~~~~~
- Fix to work with empty data bundles
- Fix Windows path of ``$HOME/.catalyst`` folder
- Fix ``etc/python2.7-environment.yml`` for Windows Conda install
- Fix hash method to create sid numbers compatible across platforms
- Fix an issue with asset date in chunks
Build
~~~~~
- Python3 adjustments
- Added method to clean bundle folders, and remove symbols.json
- Implemented and improved unit tests
Version 0.3.1
^^^^^^^^^^^^^
**Release Date**: 2017-10-22
Bug Fixes
~~~~~~~~~
- Fixed OS-dependent path issue in data bundle
- Changed handling of empty ``auth.json``, instead of throwing an error for missing file
- Updated ``etc/python2.7-environment.yml`` to work with Catalyst version 0.3
- Updated ``catalyst/examples/buy_and_hodl.py`` and ``catalyst/examples/buy_low_sell_high.py`` to work with Catalyst version 0.3
Version 0.3
^^^^^^^^^^^
**Release Date**: 2017-10-20
- Standardized live and backtesting syntax
- Added a repository for historical data
- Added supported for multiple exchanges per algorithm
- Added a standardized dictionary of symbols for each exchange
- Added auto-ingestion of bundle data while backtesting
- Bug fixes
Version 0.2.dev5
^^^^^^^^^^^^^^^^
**Release Date**: 2017-10-03
- Fixes bug in data.history function that was formatting 'volume' data as integers, now they are returned as floats with up to 9 decimals of precision. Data bundles redone.
Version 0.2.dev4
^^^^^^^^^^^^^^^^
**Release Date**: 2017-09-20
- Fixes bug in the pricing resolution of 1-minute data, now set to 8 decimal places. Pricing resolution of daily data remains set to 9 decimal places.
- The current data bundle takes 340MB compressed for download, and 460MB uncompressed on disk for Catalyst to use.
Version 0.2.dev3
^^^^^^^^^^^^^^^^
**Release Date**: 2017-09-20
- 1-minute resolution OHLCV data bundle for backtesting from Poloniex exchange
- Implementation of trading of fractional crypto assets (i.e. 0.01 BTC)
- Minimum trade size of a coin can be configured on a per-coin basis, defaults to 0.00000001 in backtesting (most exchanges set the minimum trade to larger amounts, which will impact live trading)
- Increased pricing resolution from 3 to 9 decimal places
- The current data bundle takes 40MB compressed for download, and 99MB uncompressed on disk for Catalyst to use.
Version 0.2.dev2
^^^^^^^^^^^^^^^^
**Release Date**: 2017-09-07
- Fix path issue
Version 0.2.dev1
^^^^^^^^^^^^^^^^
**Release Date**: 2017-09-03
- Implementation of live trading:
- Comprehensive trading functionality against exchanges Bitfinex and Bittrex.
- Support for all trading pairs available on each exchange.
- Multiple algorithms can trade simultaneously against a single exchange using the same account.
- Each algorithm has a persisted state (i.e. algorithm can be stopped and restarted preserving the state without data loss) that tracks all open orders, executed transactions and portfolio positions.
- Minute by minute portfolio performance metrics.
- Daily summary performance statistics compatible with pyfolio, a Python library for performance and risk analysis of financial portfolios
Version 0.1.dev9
^^^^^^^^^^^^^^^^
**Release Date**: 2017-08-28
- Retrieval of crypto benchmark from bundle, instead of hitting Poloniex exchange directly
- Change of bundle storage provider from Dropbox to AWS
- Fix issue with 1/1000 scaling issue of prices in bundle
Version 0.1.dev8
^^^^^^^^^^^^^^^^
**Release Date**: 2017-08-18
- Fixes issue in the creation of bundles (:issue:`27`)
Version 0.1.dev7
^^^^^^^^^^^^^^^^
- Fixes issues in empty benchmark (:issue:`16`)
- Fixes issue of normalizing timestamps before comparison (:issue:`24`)
- Generic data bundles
- CLI UI improvements
Version 0.1.dev6
^^^^^^^^^^^^^^^^
**Release Date**: 2017-07-13
- Initial public release
.. include:: whatsnew/0.6.1.txt
+26
View File
@@ -0,0 +1,26 @@
Resources
=========
- `Catalyst Whitepaper <https://www.enigma.co/enigma_catalyst.pdf>`_
Related 3rd Party APIs
^^^^^^^^^^^^^^^^^^^^^^
- `Zipline <http://www.zipline.io/appendix.html>`_ is a Pythonic Algorithmic
Trading Library, and the project Catalyst forked off in the spring of 2017.
- `Quantopian <https://www.quantopian.com/help>`_ provides a platform for
freelance quantitative analysts develop, test, and use trading algorithms to
buy and sell securities. They aim to create a crowd-sourced hedge fund by
fostering their community of freelance traders. Quantopian's backtesting and
live-trading engine is powered by *Zipline*.
- `Pandas <https://pandas.pydata.org/pandas-docs/stable/api.html>`_ is a Python
library providing high-performance, easy-to-use data structures and data
analysis tools. Catalyst relies heavily on pandas, and many API functions
return data as Pandas dataframes.
- `Numpy <https://docs.scipy.org/doc/numpy/reference/>`_ is the fundamental
package for scientific computing with Python. Some of the data computation
that your algorithms will need, will be optimized leveraging Numpy.
- `Matplotlib <https://matplotlib.org/1.5.3/api/index.html>`_ is a Python 2D
plotting library that many of examples rely on to plot the performance of
trading algorithms
+26
View File
@@ -0,0 +1,26 @@
Videos
======
Installation: MacOS
-------------------
.. raw:: html
<iframe width="560" height="315" src="https://www.youtube.com/embed/ZnsslmHljvw" frameborder="0" allowfullscreen></iframe>
|
|
Installation: Windows
---------------------
Where things go smoothly:
.. raw:: html
<iframe width="560" height="315" src="https://www.youtube.com/embed/H8HqcEbZmkk" frameborder="0" allowfullscreen></iframe>
|
Where things don't:
Coming up next!
+20 -5
View File
@@ -1,9 +1,22 @@
.. image:: https://s3.amazonaws.com/enigmaco-docs/enigma-catalyst.jpg
|
Catalyst is a data-driven crypto investment platform. It supports both
backtesting and live-trading in a number of different crypto-exchanges.
Catalyst empowers users to share and curate data and build profitable,
data-driven investment strategies.
Catalyst is an algorithmic trading library for crypto-assets written in Python.
It allows trading strategies to be easily expressed and backtested against
historical data (with daily and minute resolution), providing analytics and
insights regarding a particular strategy's performance. Catalyst also supports
live-trading of crypto-assets starting with three exchanges (Bitfinex, Bittrex,
and Poloniex) with more being added over time. Catalyst empowers users to share
and curate data and build profitable, data-driven investment strategies. Please
visit `enigma.co <https://www.enigma.co>`_ to learn more about Catalyst, or
refer to the `whitepaper <https://www.enigma.co/enigma_catalyst.pdf>`_ for
further technical details.
Catalyst builds on top of the well-established
`Zipline <https://github.com/quantopian/zipline>`_ project. We did our best to
minimize structural changes to the general API to maximize compatibility with
existing trading algorithms, developer knowledge, and tutorials. Join us on
`Discord <https://discord.gg/SJK32GY>`_ where we have a *#catalyst_dev* channel
for questions around Catalyst, algorithmic trading and technical support.
Features
========
@@ -25,4 +38,6 @@ Features
integrate nicely into the existing PyData eco-system.
- Statistic and machine learning libraries like matplotlib, scipy,
statsmodels, and sklearn support development, analysis, and
visualization of state-of-the-art trading systems.
visualization of state-of-the-art trading systems.
- Addition of Bitcoin price (btc_usdt) as a benchmark for comparing
performance across trading algorithms.
View File
-1
View File
@@ -1,4 +1,3 @@
import unittest
from abc import ABCMeta, abstractmethod
+2 -2
View File
@@ -8,7 +8,7 @@ from catalyst.finance.execution import (LimitOrder)
log = Logger('test_bitfinex')
class TestBitfinexTestCase(BaseExchangeTestCase):
class TestBitfinex(BaseExchangeTestCase):
@classmethod
def setup(self):
log.info('creating bitfinex object')
@@ -48,7 +48,7 @@ class TestBitfinexTestCase(BaseExchangeTestCase):
def test_get_candles(self):
log.info('retrieving candles')
ohlcv_neo = self.exchange.get_candles(
data_frequency='1m',
freq='1T',
assets=self.exchange.get_asset('neo_btc')
)
pass
+2 -2
View File
@@ -52,13 +52,13 @@ class TestBittrex(BaseExchangeTestCase):
def test_get_candles(self):
log.info('retrieving candles')
ohlcv_neo = self.exchange.get_candles(
data_frequency='5m',
freq='5T',
assets=self.exchange.get_asset('neo_btc'),
bar_count=20,
end_dt=pd.to_datetime('2017-10-20', utc=True)
)
ohlcv_neo_ubq = self.exchange.get_candles(
data_frequency='1d',
freq='1D',
assets=[
self.exchange.get_asset('neo_btc'),
self.exchange.get_asset('ubq_btc')
+64 -8
View File
@@ -1,17 +1,20 @@
import hashlib
import tempfile
from logging import getLogger
import os
import pandas as pd
from catalyst import get_calendar
from catalyst.exchange.bundle_utils import get_bcolz_chunk, \
get_periods_range, get_start_dt
get_periods_range, get_start_dt, get_month_start_end, get_df_from_arrays, \
get_year_start_end
from catalyst.exchange.exchange_bcolz import BcolzExchangeBarReader, \
BcolzExchangeBarWriter
from catalyst.exchange.exchange_bundle import ExchangeBundle, \
BUNDLE_NAME_TEMPLATE
from catalyst.exchange.exchange_utils import get_exchange_folder
from catalyst.exchange.init_utils import get_exchange
from catalyst.exchange.factory import get_exchange
from catalyst.exchange.stats_utils import df_to_string
from catalyst.utils.paths import ensure_directory
@@ -45,11 +48,11 @@ class TestExchangeBundle:
exchange = get_exchange(exchange_name)
exchange_bundle = ExchangeBundle(exchange)
assets = [
exchange.get_asset('iot_btc')
exchange.get_asset('xmr_btc')
]
# start = pd.to_datetime('2017-09-01', utc=True)
start = pd.to_datetime('2017-9-01', utc=True)
start = pd.to_datetime('2016-01-01', utc=True)
end = pd.to_datetime('2017-9-30', utc=True)
log.info('ingesting exchange bundle {}'.format(exchange_name))
@@ -123,9 +126,9 @@ class TestExchangeBundle:
# data_frequency = 'daily'
# include_symbols = 'neo_btc,bch_btc,eth_btc'
exchange_name = 'bittrex'
exchange_name = 'poloniex'
data_frequency = 'daily'
include_symbols = 'wings_eth'
include_symbols = 'eth_btc'
start = pd.to_datetime('2017-1-1', utc=True)
end = pd.to_datetime('2017-10-16', utc=True)
@@ -339,7 +342,7 @@ class TestExchangeBundle:
assets=assets,
end_dt=end_dt,
bar_count=bar_count,
data_frequency='minute'
freq='1T'
)
start_dt = get_start_dt(end_dt, bar_count, data_frequency)
@@ -389,7 +392,7 @@ class TestExchangeBundle:
start_dt=start_dt,
end_dt=end_dt,
bar_count=bar_count,
data_frequency=data_frequency
freq='1T'
)
writer = bundle.get_writer(start_dt, end_dt, data_frequency)
@@ -426,3 +429,56 @@ class TestExchangeBundle:
df = pd.DataFrame(bundle_series)
print('\n' + df_to_string(df))
pass
def bundle_to_csv(self):
exchange_name = 'bitfinex'
data_frequency = 'daily'
period = '2016'
exchange = get_exchange(exchange_name)
bundle = ExchangeBundle(exchange)
asset = exchange.get_asset('eth_btc')
path = get_bcolz_chunk(
exchange_name=exchange.name,
symbol=asset.symbol,
data_frequency=data_frequency,
period=period
)
reader = bundle.get_reader(data_frequency, path=path)
start_dt = reader.first_trading_day
end_dt = reader.last_available_dt
if data_frequency == 'daily':
end_dt = end_dt - pd.Timedelta(hours=23, minutes=59)
arrays = None
try:
arrays = reader.load_raw_arrays(
sids=[asset.sid],
fields=['open', 'high', 'low', 'close', 'volume'],
start_dt=start_dt,
end_dt=end_dt
)
except Exception as e:
log.warn('skipping ctable for {} from {} to {}: {}'.format(
asset.symbol, start_dt, end_dt, e
))
periods = bundle.get_calendar_periods_range(
start_dt, end_dt, data_frequency
)
df = get_df_from_arrays(arrays, periods)
folder = os.path.join(
tempfile.gettempdir(), 'catalyst', exchange.name, asset.symbol
)
ensure_directory(folder)
path = os.path.join(folder, period + '.csv')
log.info('creating csv file: {}'.format(path))
print('HEAD\n{}'.format(df.head(10)))
print('TAIL\n{}'.format(df.tail(10)))
df.to_csv(path)
pass
+28 -21
View File
@@ -1,47 +1,37 @@
import pandas as pd
from catalyst.exchange.exchange_data_portal import DataPortalExchangeBacktest, \
DataPortalExchangeLive
from logbook import Logger
from test_utils import rnd_history_date_days, rnd_bar_count
from catalyst import get_calendar
from catalyst.exchange.asset_finder_exchange import AssetFinderExchange
from catalyst.exchange.bitfinex.bitfinex import Bitfinex
from catalyst.exchange.bittrex.bittrex import Bittrex
from catalyst.exchange.data_portal_exchange import DataPortalExchangeBacktest, \
DataPortalExchangeLive
from catalyst.exchange.exchange_utils import get_exchange_auth
from catalyst.exchange.exchange_utils import get_exchange_auth, \
get_common_assets
from catalyst.exchange.factory import get_exchange, get_exchanges
log = Logger('test_bitfinex')
class TestExchangeDataPortalTestCase:
class TestExchangeDataPortal:
@classmethod
def setup(self):
log.info('creating bitfinex exchange')
auth_bitfinex = get_exchange_auth('bitfinex')
self.bitfinex = Bitfinex(
key=auth_bitfinex['key'],
secret=auth_bitfinex['secret'],
base_currency='usd'
)
log.info('creating bittrex exchange')
auth_bitfinex = get_exchange_auth('bittrex')
self.bittrex = Bittrex(
key=auth_bitfinex['key'],
secret=auth_bitfinex['secret'],
base_currency='usd'
)
exchanges = get_exchanges(['bitfinex', 'bittrex', 'poloniex'])
open_calendar = get_calendar('OPEN')
asset_finder = AssetFinderExchange()
self.data_portal_live = DataPortalExchangeLive(
exchanges=dict(bitfinex=self.bitfinex, bittrex=self.bittrex),
exchanges=exchanges,
asset_finder=asset_finder,
trading_calendar=open_calendar,
first_trading_day=pd.to_datetime('today', utc=True)
)
self.data_portal_backtest = DataPortalExchangeBacktest(
exchanges=dict(bitfinex=self.bitfinex),
exchanges=exchanges,
asset_finder=asset_finder,
trading_calendar=open_calendar,
first_trading_day=None # will set dynamically based on assets
@@ -106,3 +96,20 @@ class TestExchangeDataPortalTestCase:
assets, 'close', date, 'minute')
log.info('found spot value {}'.format(value))
pass
def test_history_compare_exchanges(self):
exchanges = get_exchanges(['bittrex', 'bitfinex', 'poloniex'])
assets = get_common_assets(exchanges)
date = rnd_history_date_days()
bar_count = rnd_bar_count()
data = self.data_portal_backtest.get_history_window(
assets=assets,
end_dt=date,
bar_count=bar_count,
frequency='1d',
field='close',
data_frequency='daily'
)
log.info('found history window: {}'.format(data))
+4 -4
View File
@@ -8,7 +8,7 @@ from catalyst.exchange.exchange_utils import get_exchange_auth
log = Logger('test_poloniex')
class TestPoloniexTestCase(BaseExchangeTestCase):
class TestPoloniex(BaseExchangeTestCase):
@classmethod
def setup(self):
print ('creating poloniex object')
@@ -52,11 +52,11 @@ class TestPoloniexTestCase(BaseExchangeTestCase):
def test_get_candles(self):
log.info('retrieving candles')
ohlcv_neo = self.exchange.get_candles(
data_frequency='5m',
assets=self.exchange.get_asset('neos_btc')
freq='5T',
assets=self.exchange.get_asset('eth_btc')
)
ohlcv_neo_ubq = self.exchange.get_candles(
data_frequency='5m',
freq='5T',
assets=[
self.exchange.get_asset('neos_btc'),
self.exchange.get_asset('via_btc')
+124
View File
@@ -0,0 +1,124 @@
import os
import tarfile
import importlib
import pandas as pd
from catalyst import get_calendar
from catalyst.exchange.exchange_bundle import ExchangeBundle
from catalyst.exchange.exchange_bcolz import BcolzExchangeBarReader
from catalyst.data.minute_bars import BcolzMinuteBarMetadata
from catalyst.exchange.bundle_utils import get_df_from_arrays, get_bcolz_chunk
import matplotlib
import matplotlib.pyplot as plt
from matplotlib.finance import candlestick2_ohlc
from matplotlib.finance import volume_overlay
import matplotlib.ticker as ticker
from catalyst.exchange.factory import get_exchange
EXCHANGE_NAMES = ['bitfinex', 'bittrex', 'poloniex']
exchanges = dict((e, getattr(importlib.import_module(
'catalyst.exchange.{0}.{0}'.format(e)), e.capitalize()))
for e in EXCHANGE_NAMES)
class ValidateChunks(object):
def __init__(self):
self.columns = ['open', 'high', 'low', 'close', 'volume']
def chunk_to_df(self, exchange_name, symbol, data_frequency, period):
exchange = get_exchange(exchange_name)
asset = exchange.get_asset(symbol)
filename = get_bcolz_chunk(
exchange_name=exchange_name,
symbol=symbol,
data_frequency=data_frequency,
period=period
)
reader = BcolzExchangeBarReader(rootdir=filename,
data_frequency=data_frequency)
# metadata = BcolzMinuteBarMetadata.read(filename)
start = reader.first_trading_day
end = reader.last_available_dt
if data_frequency == 'daily':
end = end - pd.Timedelta(hours=23, minutes=59)
print start, end, data_frequency
arrays = reader.load_raw_arrays(self.columns, start, end,
[asset.sid, ])
bundle = ExchangeBundle(exchange_name)
periods = bundle.get_calendar_periods_range(
start, end, data_frequency
)
return get_df_from_arrays(arrays, periods)
def plot_ohlcv(self, df):
fig, ax = plt.subplots()
# Plot the candlestick
candlestick2_ohlc(ax, df['open'], df['high'], df['low'], df['close'],
width=1, colorup='g', colordown='r', alpha=0.5)
# shift y-limits of the candlestick plot so that there is space
# at the bottom for the volume bar chart
pad = 0.25
yl = ax.get_ylim()
ax.set_ylim(yl[0] - (yl[1] - yl[0]) * pad, yl[1])
# Add a seconds axis for the volume overlay
ax2 = ax.twinx()
ax2.set_position(
matplotlib.transforms.Bbox([[0.125, 0.1], [0.9, 0.26]]))
# Plot the volume overlay
bc = volume_overlay(ax2, df['open'], df['close'], df['volume'],
colorup='g', alpha=0.5, width=1)
ax.xaxis.set_major_locator(ticker.MaxNLocator(6))
def mydate(x, pos):
try:
return df.index[int(x)]
except IndexError:
return ''
ax.xaxis.set_major_formatter(ticker.FuncFormatter(mydate))
plt.margins(0)
plt.show()
def plot(self, filename):
df = self.chunk_to_df(filename)
self.plot_ohlcv(df)
def to_csv(self, filename):
df = self.chunk_to_df(filename)
df.to_csv(os.path.basename(filename).split('.')[0] + '.csv')
v = ValidateChunks()
df = v.chunk_to_df(
exchange_name='bitfinex',
symbol='eth_btc',
data_frequency='daily',
period='2016'
)
print(df.tail())
v.plot_ohlcv(df)
# v.plot(
# ex
# )
+17
View File
@@ -0,0 +1,17 @@
from datetime import timedelta
from random import randint
import pandas as pd
def rnd_history_date_days(max_days=30):
now = pd.Timestamp.utcnow()
days = randint(0, max_days)
return now - timedelta(days=days)
def rnd_bar_count(max_bars=21):
now = pd.Timestamp.utcnow()
return randint(0, max_bars)