mirror of
https://github.com/wassname/catalyst.git
synced 2026-08-18 11:50:11 +08:00
Working on multiple exchanges and a sample algo for arbitrage
This commit is contained in:
@@ -0,0 +1,190 @@
|
|||||||
|
import talib
|
||||||
|
from logbook import Logger
|
||||||
|
|
||||||
|
from catalyst.api import (
|
||||||
|
order,
|
||||||
|
order_target_percent,
|
||||||
|
symbol,
|
||||||
|
record,
|
||||||
|
get_open_orders,
|
||||||
|
)
|
||||||
|
from catalyst.exchange.stats_utils import get_pretty_stats
|
||||||
|
from catalyst.utils.run_algo import run_algorithm
|
||||||
|
|
||||||
|
algo_namespace = 'arbitrage_neo_eth'
|
||||||
|
log = Logger(algo_namespace)
|
||||||
|
|
||||||
|
|
||||||
|
def initialize(context):
|
||||||
|
log.info('initializing arbitrage algorithm')
|
||||||
|
|
||||||
|
context.buying_exchange = 'bittrex'
|
||||||
|
context.selling_exchange = 'bitfinex'
|
||||||
|
|
||||||
|
context.trading_pair_symbol = 'neo_eth'
|
||||||
|
context.trading_pairs = dict()
|
||||||
|
context.trading_pairs[context.buying_exchange] = \
|
||||||
|
symbol(context.trading_pair_symbol, context.buying_exchange)
|
||||||
|
context.trading_pairs[context.selling_exchange] = \
|
||||||
|
symbol(context.trading_pair_symbol, context.selling_exchange)
|
||||||
|
|
||||||
|
context.entry_points = [
|
||||||
|
dict(gap=0.001, amount=0.05),
|
||||||
|
dict(gap=0.002, amount=0.1),
|
||||||
|
]
|
||||||
|
context.exit_points = [
|
||||||
|
dict(gap=0, amount=0.05),
|
||||||
|
dict(gap=-0.001, amount=0.01),
|
||||||
|
]
|
||||||
|
|
||||||
|
context.MAX_POSITIONS = 50
|
||||||
|
context.SLIPPAGE_ALLOWED = 0.02
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def place_order(context, amount, buying_price, selling_price,
|
||||||
|
action):
|
||||||
|
if action == 'enter':
|
||||||
|
buying_exchange = context.exchanges[context.buying_exchange]
|
||||||
|
buy_price = buying_price
|
||||||
|
|
||||||
|
selling_exchange = context.exchanges[context.selling_exchange]
|
||||||
|
sell_price = selling_price
|
||||||
|
|
||||||
|
elif action == 'exit':
|
||||||
|
buying_exchange = context.exchanges[context.selling_exchange]
|
||||||
|
buy_price = selling_price
|
||||||
|
|
||||||
|
selling_exchange = context.exchanges[context.buying_exchange]
|
||||||
|
sell_price = buying_price
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ValueError('invalid order action')
|
||||||
|
|
||||||
|
base_currency = buying_exchange.base_currency
|
||||||
|
base_currency_amount = buying_exchange.portfolio.cash
|
||||||
|
|
||||||
|
sell_balances = selling_exchange.get_balances()
|
||||||
|
sell_currency = context.trading_pairs[
|
||||||
|
context.selling_exchange].market_currency
|
||||||
|
|
||||||
|
if sell_currency in sell_balances:
|
||||||
|
market_currency_amount = sell_balances[sell_currency]
|
||||||
|
else:
|
||||||
|
log.warn('the selling exchange {} does not hold currency {}'.format(
|
||||||
|
selling_exchange.name, sell_currency
|
||||||
|
))
|
||||||
|
return
|
||||||
|
|
||||||
|
if base_currency_amount < amount:
|
||||||
|
log.warn('not enough {} ({}) to buy {}, adjusting the amount'.format(
|
||||||
|
base_currency, base_currency_amount, amount))
|
||||||
|
amount = base_currency_amount
|
||||||
|
elif market_currency_amount < amount:
|
||||||
|
log.warn('not enough {} ({}) to sell {}, aborting'.format(
|
||||||
|
sell_currency, market_currency_amount, amount))
|
||||||
|
return
|
||||||
|
|
||||||
|
adj_buy_price = buy_price * (1 + context.SLIPPAGE_ALLOWED)
|
||||||
|
log.info('buying {} limit at {}{} on {}'.format(
|
||||||
|
amount, buying_price, context.trading_pair_symbol,
|
||||||
|
buying_exchange.name))
|
||||||
|
order(
|
||||||
|
asset=context.trading_pairs[buying_exchange],
|
||||||
|
amount=amount,
|
||||||
|
limit_price=adj_buy_price
|
||||||
|
)
|
||||||
|
|
||||||
|
adj_sell_price = sell_price * (1 - context.SLIPPAGE_ALLOWED)
|
||||||
|
log.info('selling {} limit at {}{} on {}'.format(
|
||||||
|
amount, adj_sell_price, context.trading_pair_symbol,
|
||||||
|
selling_exchange.name))
|
||||||
|
order(
|
||||||
|
asset=context.trading_pairs[selling_exchange],
|
||||||
|
amount=amount,
|
||||||
|
limit_price=adj_sell_price
|
||||||
|
)
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def handle_data(context, data):
|
||||||
|
log.info('handling bar {}'.format(data.current_dt))
|
||||||
|
|
||||||
|
buying_price = data.current(
|
||||||
|
context.trading_pairs[context.buying_exchange], 'price')
|
||||||
|
log.info('price on buying exchange {exchange}: {price}'.format(
|
||||||
|
exchange=context.buying_exchange.upper(),
|
||||||
|
price=buying_price,
|
||||||
|
))
|
||||||
|
|
||||||
|
selling_price = data.current(
|
||||||
|
context.trading_pairs[context.selling_exchange], 'price')
|
||||||
|
|
||||||
|
log.info('price on selling exchange {exchange}: {price}'.format(
|
||||||
|
exchange=context.selling_exchange.upper(),
|
||||||
|
price=selling_price,
|
||||||
|
))
|
||||||
|
|
||||||
|
# If for example,
|
||||||
|
# selling price = 50
|
||||||
|
# buying price = 25
|
||||||
|
# expected gap = 1
|
||||||
|
|
||||||
|
# If follows that,
|
||||||
|
# selling price - buying price / buying price
|
||||||
|
# 50 - 25 / 25 = 1
|
||||||
|
gap = (selling_price - buying_price) / buying_price
|
||||||
|
log.info('the price gap: {} ({}%)'.format(gap, gap * 100))
|
||||||
|
|
||||||
|
# Consider the least ambitious entry point first
|
||||||
|
# Override of wider gap is found
|
||||||
|
entry_points = sorted(
|
||||||
|
context.entry_points,
|
||||||
|
key=lambda point: point['gap'],
|
||||||
|
)
|
||||||
|
|
||||||
|
buy_amount = None
|
||||||
|
for entry_point in entry_points:
|
||||||
|
if gap > entry_point['gap']:
|
||||||
|
buy_amount = entry_point['amount']
|
||||||
|
|
||||||
|
if buy_amount:
|
||||||
|
log.info('found buy trigger for amount: {}'.format(buy_amount))
|
||||||
|
place_order(context, buy_amount, buying_price, selling_price, 'enter')
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Consider the narrowest exit gap first
|
||||||
|
# Override of wider gap is found
|
||||||
|
exit_points = sorted(
|
||||||
|
context.exit_points,
|
||||||
|
key=lambda point: point['gap'],
|
||||||
|
reverse=True
|
||||||
|
)
|
||||||
|
|
||||||
|
sell_amount = None
|
||||||
|
for exit_point in exit_points:
|
||||||
|
if gap < exit_point['gap']:
|
||||||
|
sell_amount = exit_point['amount']
|
||||||
|
|
||||||
|
if sell_amount:
|
||||||
|
log.info('found sell trigger for amount: {}'.format(sell_amount))
|
||||||
|
place_order(context, sell_amount, buying_price, selling_price,
|
||||||
|
'exit')
|
||||||
|
|
||||||
|
|
||||||
|
def analyze(context, stats):
|
||||||
|
log.info('the daily stats:\n{}'.format(get_pretty_stats(stats)))
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
run_algorithm(
|
||||||
|
initialize=initialize,
|
||||||
|
handle_data=handle_data,
|
||||||
|
analyze=analyze,
|
||||||
|
exchange_name='bittrex,bitfinex',
|
||||||
|
live=True,
|
||||||
|
algo_namespace=algo_namespace,
|
||||||
|
base_currency='eth',
|
||||||
|
live_graph=False
|
||||||
|
)
|
||||||
@@ -11,42 +11,43 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
import os
|
import os
|
||||||
|
import pickle
|
||||||
import signal
|
import signal
|
||||||
import sys
|
import sys
|
||||||
import pickle
|
from collections import deque
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from time import sleep
|
|
||||||
from os import listdir
|
from os import listdir
|
||||||
from os.path import isfile, join
|
from os.path import isfile, join
|
||||||
from collections import deque
|
from time import sleep
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
import logbook
|
import logbook
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from catalyst.utils.preprocess import preprocess
|
from catalyst.assets._assets import TradingPair
|
||||||
|
|
||||||
import catalyst.protocol as zp
|
import catalyst.protocol as zp
|
||||||
from catalyst.algorithm import TradingAlgorithm
|
from catalyst.algorithm import TradingAlgorithm
|
||||||
from catalyst.data.minute_bars import BcolzMinuteBarWriter, \
|
from catalyst.data.minute_bars import BcolzMinuteBarWriter, \
|
||||||
BcolzMinuteBarReader
|
BcolzMinuteBarReader
|
||||||
from catalyst.errors import OrderInBeforeTradingStart
|
from catalyst.errors import OrderInBeforeTradingStart
|
||||||
from catalyst.exchange.simple_clock import SimpleClock
|
|
||||||
from catalyst.exchange.live_graph_clock import LiveGraphClock
|
|
||||||
from catalyst.exchange.exchange_errors import (
|
from catalyst.exchange.exchange_errors import (
|
||||||
ExchangeRequestError,
|
ExchangeRequestError,
|
||||||
ExchangePortfolioDataError,
|
ExchangePortfolioDataError,
|
||||||
ExchangeTransactionError
|
ExchangeTransactionError,
|
||||||
)
|
OrphanOrderError)
|
||||||
from catalyst.exchange.exchange_utils import get_exchange_minute_writer_root, \
|
from catalyst.exchange.exchange_utils import get_exchange_minute_writer_root, \
|
||||||
save_algo_object, get_algo_object, get_algo_folder, get_algo_df, \
|
save_algo_object, get_algo_object, get_algo_folder, get_algo_df, \
|
||||||
save_algo_df
|
save_algo_df
|
||||||
|
from catalyst.exchange.live_graph_clock import LiveGraphClock
|
||||||
|
from catalyst.exchange.simple_clock import SimpleClock
|
||||||
from catalyst.exchange.stats_utils import get_pretty_stats
|
from catalyst.exchange.stats_utils import get_pretty_stats
|
||||||
from catalyst.finance.performance.period import calc_period_stats
|
from catalyst.finance.performance.period import calc_period_stats
|
||||||
from catalyst.gens.tradesimulation import AlgorithmSimulator
|
from catalyst.gens.tradesimulation import AlgorithmSimulator
|
||||||
from catalyst.utils.api_support import (
|
from catalyst.utils.api_support import (
|
||||||
api_method,
|
api_method,
|
||||||
disallowed_in_before_trading_start)
|
disallowed_in_before_trading_start)
|
||||||
from catalyst.utils.input_validation import error_keywords, ensure_upper_case
|
from catalyst.utils.input_validation import error_keywords, ensure_upper_case, \
|
||||||
|
expect_types
|
||||||
|
from catalyst.utils.preprocess import preprocess
|
||||||
|
|
||||||
log = logbook.Logger("ExchangeTradingAlgorithm")
|
log = logbook.Logger("ExchangeTradingAlgorithm")
|
||||||
|
|
||||||
@@ -406,7 +407,9 @@ class ExchangeTradingAlgorithm(TradingAlgorithm):
|
|||||||
self.minute_stats.append(minute_stats)
|
self.minute_stats.append(minute_stats)
|
||||||
|
|
||||||
self.add_pnl_stats(minute_stats)
|
self.add_pnl_stats(minute_stats)
|
||||||
self.add_custom_signals_stats(minute_stats)
|
if self.recorded_vars:
|
||||||
|
self.add_custom_signals_stats(minute_stats)
|
||||||
|
|
||||||
self.add_exposure_stats(minute_stats)
|
self.add_exposure_stats(minute_stats)
|
||||||
|
|
||||||
print_df = pd.DataFrame(list(self.minute_stats))
|
print_df = pd.DataFrame(list(self.minute_stats))
|
||||||
@@ -481,23 +484,50 @@ class ExchangeTradingAlgorithm(TradingAlgorithm):
|
|||||||
|
|
||||||
@api_method
|
@api_method
|
||||||
@disallowed_in_before_trading_start(OrderInBeforeTradingStart())
|
@disallowed_in_before_trading_start(OrderInBeforeTradingStart())
|
||||||
|
@expect_types(asset=TradingPair)
|
||||||
def order(self,
|
def order(self,
|
||||||
asset,
|
asset,
|
||||||
amount,
|
amount,
|
||||||
limit_price=None,
|
limit_price=None,
|
||||||
stop_price=None,
|
stop_price=None,
|
||||||
style=None):
|
style=None):
|
||||||
|
"""
|
||||||
|
We use the exchange specific portfolio to place orders.
|
||||||
|
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
|
||||||
|
The catalyst order object or None
|
||||||
|
"""
|
||||||
|
|
||||||
amount, style = self._calculate_order(asset, amount,
|
amount, style = self._calculate_order(asset, amount,
|
||||||
limit_price, stop_price,
|
limit_price, stop_price,
|
||||||
style)
|
style)
|
||||||
|
|
||||||
order_id = self._order(asset, amount, limit_price, stop_price, style)
|
order_id = self._order(asset, amount, limit_price, stop_price, style)
|
||||||
|
|
||||||
|
exchange = self.exchanges[asset.exchange]
|
||||||
|
exchange_portfolio = exchange.portfolio
|
||||||
if order_id is not None:
|
if order_id is not None:
|
||||||
order = self.portfolio.open_orders[order_id]
|
|
||||||
self.perf_tracker.process_order(order)
|
if order_id in exchange_portfolio.open_orders:
|
||||||
return order
|
order = exchange_portfolio.open_orders[order_id]
|
||||||
|
self.perf_tracker.process_order(order)
|
||||||
|
return order
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise OrphanOrderError(
|
||||||
|
order_id=order_id,
|
||||||
|
exchange=exchange.name
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
|
log.warn('unable to order {} {} on exchange {}'.format(
|
||||||
|
amount, asset.symbol, asset.exchange))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def round_order(self, amount):
|
def round_order(self, amount):
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ class Bitfinex(Exchange):
|
|||||||
self.key = key
|
self.key = key
|
||||||
self.secret = secret.encode('UTF-8')
|
self.secret = secret.encode('UTF-8')
|
||||||
self.name = 'bitfinex'
|
self.name = 'bitfinex'
|
||||||
|
self.color = 'green'
|
||||||
self.assets = {}
|
self.assets = {}
|
||||||
self.load_assets()
|
self.load_assets()
|
||||||
self.base_currency = base_currency
|
self.base_currency = base_currency
|
||||||
|
|||||||
@@ -1,4 +1,16 @@
|
|||||||
{
|
{
|
||||||
|
"neobtc": {
|
||||||
|
"symbol": "neo_btc",
|
||||||
|
"start_date": "2017-09-07"
|
||||||
|
},
|
||||||
|
"neousd": {
|
||||||
|
"symbol": "neo_usd",
|
||||||
|
"start_date": "2017-09-07"
|
||||||
|
},
|
||||||
|
"neoeth": {
|
||||||
|
"symbol": "neo_eth",
|
||||||
|
"start_date": "2017-09-07"
|
||||||
|
},
|
||||||
"btcusd": {
|
"btcusd": {
|
||||||
"symbol": "btc_usd",
|
"symbol": "btc_usd",
|
||||||
"start_date": "2010-01-01"
|
"start_date": "2010-01-01"
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ class Bittrex(Exchange):
|
|||||||
def __init__(self, key, secret, base_currency, portfolio=None):
|
def __init__(self, key, secret, base_currency, portfolio=None):
|
||||||
self.api = Bittrex_api(key=key, secret=secret.encode('UTF-8'))
|
self.api = Bittrex_api(key=key, secret=secret.encode('UTF-8'))
|
||||||
self.name = 'bittrex'
|
self.name = 'bittrex'
|
||||||
|
self.color = 'blue'
|
||||||
self.base_currency = base_currency
|
self.base_currency = base_currency
|
||||||
self._portfolio = portfolio
|
self._portfolio = portfolio
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from catalyst.errors import (
|
|||||||
SymbolNotFound,
|
SymbolNotFound,
|
||||||
)
|
)
|
||||||
from catalyst.exchange.exchange_errors import MismatchingBaseCurrencies, \
|
from catalyst.exchange.exchange_errors import MismatchingBaseCurrencies, \
|
||||||
InvalidOrderStyle, BaseCurrencyNotFoundError
|
InvalidOrderStyle, BaseCurrencyNotFoundError, SymbolNotFoundOnExchange
|
||||||
from catalyst.exchange.exchange_execution import ExchangeStopLimitOrder, \
|
from catalyst.exchange.exchange_execution import ExchangeStopLimitOrder, \
|
||||||
ExchangeLimitOrder, ExchangeStopOrder
|
ExchangeLimitOrder, ExchangeStopOrder
|
||||||
from catalyst.exchange.exchange_portfolio import ExchangePortfolio
|
from catalyst.exchange.exchange_portfolio import ExchangePortfolio
|
||||||
@@ -30,7 +30,6 @@ class Exchange:
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.name = None
|
self.name = None
|
||||||
self.trading_pairs = None
|
|
||||||
self.assets = {}
|
self.assets = {}
|
||||||
self._portfolio = None
|
self._portfolio = None
|
||||||
self.minute_writer = None
|
self.minute_writer = None
|
||||||
@@ -110,7 +109,12 @@ class Exchange:
|
|||||||
asset = self.assets[key]
|
asset = self.assets[key]
|
||||||
|
|
||||||
if not asset:
|
if not asset:
|
||||||
raise SymbolNotFound(symbol=symbol)
|
supported_symbols = [pair.symbol for pair in self.assets.values()]
|
||||||
|
raise SymbolNotFoundOnExchange(
|
||||||
|
symbol=symbol,
|
||||||
|
exchange=self.name,
|
||||||
|
supported_symbols=supported_symbols
|
||||||
|
)
|
||||||
|
|
||||||
return asset
|
return asset
|
||||||
|
|
||||||
|
|||||||
@@ -94,6 +94,13 @@ class OrderNotFound(ZiplineError):
|
|||||||
).strip()
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
|
class OrphanOrderError(ZiplineError):
|
||||||
|
msg = (
|
||||||
|
'Order {order_id} found in exchange {exchange} but not tracked by '
|
||||||
|
'the algorithm.'
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
class OrderCancelError(ZiplineError):
|
class OrderCancelError(ZiplineError):
|
||||||
msg = (
|
msg = (
|
||||||
'Unable to cancel order {order_id} on exchange {exchange} {error}.'
|
'Unable to cancel order {order_id} on exchange {exchange} {error}.'
|
||||||
@@ -118,3 +125,18 @@ class MismatchingBaseCurrencies(ZiplineError):
|
|||||||
'Unable to trade with base currency {base_currency} when the '
|
'Unable to trade with base currency {base_currency} when the '
|
||||||
'algorithm uses {algo_currency}.'
|
'algorithm uses {algo_currency}.'
|
||||||
).strip()
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
|
class MismatchingBaseCurrenciesExchanges(ZiplineError):
|
||||||
|
msg = (
|
||||||
|
'Unable to trade with base currency {base_currency} when the '
|
||||||
|
'exchange {exchange_name} users {exchange_currency}.'
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
|
class SymbolNotFoundOnExchange(ZiplineError):
|
||||||
|
"""
|
||||||
|
Raised when a symbol() call contains a non-existant symbol.
|
||||||
|
"""
|
||||||
|
msg = ('Symbol {symbol} not found on exchange {exchange}. '
|
||||||
|
'Choose from: {supported_symbols}').strip()
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ from logbook import Logger
|
|||||||
from matplotlib import pyplot as plt
|
from matplotlib import pyplot as plt
|
||||||
from matplotlib import style
|
from matplotlib import style
|
||||||
|
|
||||||
|
from catalyst.exchange.exchange_errors import \
|
||||||
|
MismatchingBaseCurrenciesExchanges
|
||||||
|
|
||||||
log = Logger('LiveGraphClock')
|
log = Logger('LiveGraphClock')
|
||||||
|
|
||||||
style.use('dark_background')
|
style.use('dark_background')
|
||||||
@@ -154,17 +157,31 @@ class LiveGraphClock(object):
|
|||||||
context = self.context
|
context = self.context
|
||||||
df = context.exposure_stats
|
df = context.exposure_stats
|
||||||
|
|
||||||
|
# TODO: list exchanges in graph
|
||||||
|
base_currency = None
|
||||||
|
positions = []
|
||||||
|
for exchange_name in context.exchanges:
|
||||||
|
exchange = context.exchanges[exchange_name]
|
||||||
|
|
||||||
|
if not base_currency:
|
||||||
|
base_currency = exchange.base_currency
|
||||||
|
elif base_currency != exchange.base_currency:
|
||||||
|
raise MismatchingBaseCurrenciesExchanges(
|
||||||
|
base_currency=base_currency,
|
||||||
|
exchange_name=exchange.name,
|
||||||
|
exchange_currency=exchange.base_currency
|
||||||
|
)
|
||||||
|
|
||||||
|
positions += exchange.portfolio.positions
|
||||||
|
|
||||||
ax.clear()
|
ax.clear()
|
||||||
ax.set_title('Exposure')
|
ax.set_title('Exposure')
|
||||||
ax.plot(df.index, df['base_currency'], '-',
|
ax.plot(df.index, df['base_currency'], '-',
|
||||||
color='green',
|
color='green',
|
||||||
linewidth=1.0,
|
linewidth=1.0,
|
||||||
label='Base Currency: {}'.format(
|
label='Base Currency: {}'.format(base_currency.upper())
|
||||||
context.exchange.base_currency.upper()
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
positions = context.exchange.portfolio.positions
|
|
||||||
symbols = []
|
symbols = []
|
||||||
for position in positions:
|
for position in positions:
|
||||||
symbols.append(position.symbol)
|
symbols.append(position.symbol)
|
||||||
@@ -172,10 +189,7 @@ class LiveGraphClock(object):
|
|||||||
ax.plot(df.index, df['long_exposure'], '-',
|
ax.plot(df.index, df['long_exposure'], '-',
|
||||||
color='blue',
|
color='blue',
|
||||||
linewidth=1.0,
|
linewidth=1.0,
|
||||||
label='Long Exposure: {}'.format(
|
label='Long Exposure: {}'.format(', '.join(symbols).upper()))
|
||||||
', '.join(symbols).upper()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.set_legend(ax)
|
self.set_legend(ax)
|
||||||
self.format_ax(ax)
|
self.format_ax(ax)
|
||||||
|
|||||||
Reference in New Issue
Block a user