Files
OpenBB/openbb_terminal/forex/oanda/oanda_view.py
montezdesousa 02901f1473 Trim command output spaces (#3424)
* first round of removing spaces

* remove more print statemnts

* remove more spaces

* bullbear color

* spacc color

* improve stalker readability

* delete more spaces

* reddit colors

* rever hood

* flake8

* forgot this one

* fix econometrics tests

* fix econometrics tests

* fix more tests

Co-authored-by: James Maslek <jmaslek11@gmail.com>
2022-11-16 11:47:24 +00:00

549 lines
17 KiB
Python

"""Oanda View."""
__docformat__ = "numpy"
import logging
from typing import Dict, Union, Optional, List
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker
import mplfinance as mpf
import pandas as pd
import pandas_ta as ta
import seaborn as sns
from openbb_terminal.config_terminal import theme
from openbb_terminal.config_plot import PLOT_DPI
from openbb_terminal.decorators import check_api_key
from openbb_terminal.decorators import log_start_end
from openbb_terminal.forex.oanda.oanda_model import (
account_summary_request,
cancel_pending_order_request,
close_trades_request,
create_order_request,
fx_price_request,
get_calendar_request,
get_candles_dataframe,
open_positions_request,
open_trades_request,
order_history_request,
orderbook_plot_data_request,
pending_orders_request,
positionbook_plot_data_request,
)
from openbb_terminal.helper_funcs import plot_autoscale, is_valid_axes_count
from openbb_terminal.rich_config import console
logger = logging.getLogger(__name__)
@log_start_end(log=logger)
@check_api_key(["OANDA_ACCOUNT", "OANDA_TOKEN", "OANDA_ACCOUNT_TYPE"])
def get_fx_price(account: str, instrument: Union[str, None] = ""):
"""View price for loaded currency pair.
Parameters
----------
accountID : str
Oanda account ID
instrument : Union[str, None]
Instrument code or None
"""
data = fx_price_request(accountID=account, instrument=instrument)
if data and data is not None and "prices" in data:
bid = data["prices"][0]["bids"][0]["price"]
ask = data["prices"][0]["asks"][0]["price"]
console.print(f"{instrument if instrument else ''}" + " Bid: " + bid)
console.print(f"{instrument if instrument else ''}" + " Ask: " + ask)
else:
console.print("No data was retrieved.\n")
@log_start_end(log=logger)
@check_api_key(["OANDA_ACCOUNT", "OANDA_TOKEN", "OANDA_ACCOUNT_TYPE"])
def get_account_summary(accountID: str):
"""Print Oanda account summary.
Parameters
----------
accountID : str
Oanda user account ID
"""
df_summary = account_summary_request(accountID)
if df_summary is not False and not df_summary.empty:
console.print(df_summary.to_string(index=False, header=False))
else:
console.print("No data was retrieved.\n")
@log_start_end(log=logger)
@check_api_key(["OANDA_ACCOUNT", "OANDA_TOKEN", "OANDA_ACCOUNT_TYPE"])
def get_order_book(
accountID: str,
instrument: str = "",
external_axes: Optional[List[plt.Axes]] = None,
):
"""
Plot the orderbook for the instrument if Oanda provides one.
Parameters
----------
accountID : str
Oanda user account ID
instrument : str
The loaded currency pair
external_axes : Optional[List[plt.Axes]], optional
External axes (1 axis is expected in the list), by default None
"""
df_orderbook_data = orderbook_plot_data_request(
accountID=accountID, instrument=instrument
)
if df_orderbook_data is not False and not df_orderbook_data.empty:
pd.set_option("display.max_rows", None)
# HELP WANTED!
# TODO:
# An early contributor left "magic constants" in this function
# help is needed to figure out the rationale behind these or
# refactor it to not include the magic numbers.
df_orderbook_data = df_orderbook_data.take(range(527, 727, 1))
book_plot(
df_orderbook_data, instrument, "Order Book", external_axes=external_axes
)
else:
console.print("No data was retrieved.\n")
@log_start_end(log=logger)
@check_api_key(["OANDA_ACCOUNT", "OANDA_TOKEN", "OANDA_ACCOUNT_TYPE"])
def get_position_book(
accountID: str, instrument: str = "", external_axes: Optional[List[plt.Axes]] = None
):
"""Plot a position book for an instrument if Oanda provides one.
Parameters
----------
accountID : str
Oanda user account ID
instrument : str
The loaded currency pair
external_axes : Optional[List[plt.Axes]], optional
External axes (1 axis is expected in the list), by default None
"""
df_positionbook_data = positionbook_plot_data_request(
accountID=accountID, instrument=instrument
)
if df_positionbook_data is not False and not df_positionbook_data.empty:
pd.set_option("display.max_rows", None)
# HELP WANTED!
# TODO:
# An early contributor left "magic constants" in this function
# help is needed to figure out the rationale behind these or
# refactor it to not include the magic numbers.
df_positionbook_data = df_positionbook_data.take(range(219, 300, 1))
book_plot(
df_positionbook_data,
instrument,
"Position Book",
external_axes=external_axes,
)
else:
console.print("No data was retrieved.\n")
@log_start_end(log=logger)
@check_api_key(["OANDA_ACCOUNT", "OANDA_TOKEN", "OANDA_ACCOUNT_TYPE"])
def list_orders(accountID: str, order_state: str = "PENDING", order_count: int = 0):
"""List order history.
Parameters
----------
accountID : str
Oanda user account ID
order_state : str
Filter orders by a specific state ("PENDING", "CANCELLED", etc.)
order_count : int
Limit the number of orders to retrieve
"""
df_order_list = order_history_request(order_state, order_count, accountID)
if df_order_list is not False and not df_order_list.empty:
console.print(df_order_list)
else:
console.print("No data was retrieved.\n")
@log_start_end(log=logger)
@check_api_key(["OANDA_ACCOUNT", "OANDA_TOKEN", "OANDA_ACCOUNT_TYPE"])
def create_order(accountID: str, instrument: str = "", price: int = 0, units: int = 0):
"""Create a buy/sell order.
Parameters
----------
accountID : str
Oanda user account ID
instrument : str
The loaded currency pair
price : int
The price to set for the limit order.
units : int
The number of units to place in the order request.
"""
df_orders = create_order_request(price, units, instrument, accountID)
if df_orders is not False and not df_orders.empty:
console.print(df_orders.to_string(index=False))
else:
console.print("No data was returned from Oanda.\n")
@log_start_end(log=logger)
@check_api_key(["OANDA_ACCOUNT", "OANDA_TOKEN", "OANDA_ACCOUNT_TYPE"])
def cancel_pending_order(accountID: str, orderID: str = ""):
"""Cancel a Pending Order.
Parameters
----------
accountID : str
Oanda user account ID
orderID : str
The pending order ID to cancel.
"""
order_id = cancel_pending_order_request(orderID, accountID)
if order_id is not False and order_id is not None:
console.print(f"Order {order_id} canceled.")
else:
console.print("No data was returned from Oanda.\n")
@log_start_end(log=logger)
@check_api_key(["OANDA_ACCOUNT", "OANDA_TOKEN", "OANDA_ACCOUNT_TYPE"])
def get_open_positions(accountID: str):
"""Get information about open positions.
Parameters
----------
accountID : str
Oanda user account ID
"""
df_positions = open_positions_request(accountID)
if df_positions is not False and not df_positions.empty:
console.print(df_positions.to_string(index=False))
else:
console.print("No data was returned from Oanda.\n")
@log_start_end(log=logger)
@check_api_key(["OANDA_ACCOUNT", "OANDA_TOKEN", "OANDA_ACCOUNT_TYPE"])
def get_pending_orders(accountID: str):
"""Get information about pending orders.
Parameters
----------
accountID : str
Oanda user account ID
"""
df_pending = pending_orders_request(accountID)
if df_pending is not False and not df_pending.empty:
console.print(df_pending.to_string(index=False))
elif df_pending is not False and df_pending.empty:
console.print("No pending orders.\n")
else:
console.print("No data was returned from Oanda.\n")
@log_start_end(log=logger)
@check_api_key(["OANDA_ACCOUNT", "OANDA_TOKEN", "OANDA_ACCOUNT_TYPE"])
def get_open_trades(accountID: str):
"""View open trades.
Parameters
----------
accountID : str
Oanda user account ID
"""
df_trades = open_trades_request(accountID)
if isinstance(df_trades, pd.DataFrame) and not df_trades.empty:
console.print(df_trades.to_string(index=False))
elif df_trades is not False and df_trades.empty:
console.print("No trades were found.\n")
else:
console.print("No data was returned from Oanda.\n")
@log_start_end(log=logger)
@check_api_key(["OANDA_ACCOUNT", "OANDA_TOKEN", "OANDA_ACCOUNT_TYPE"])
def close_trade(accountID: str, orderID: str = "", units: Union[int, None] = None):
"""Close a trade.
Parameters
----------
accountID : str
Oanda user account ID
orderID : str
ID of the order to close
units : Union[int, None]
Number of units to close. If empty default to all.
"""
df_trades = close_trades_request(orderID, units, accountID)
if df_trades is not False and not df_trades.empty:
console.print(df_trades.to_string(index=False))
elif df_trades is not False and df_trades.empty:
console.print("No trades were found.\n")
else:
console.print("No data was returned from Oanda.\n")
@log_start_end(log=logger)
@check_api_key(["OANDA_ACCOUNT", "OANDA_TOKEN", "OANDA_ACCOUNT_TYPE"])
def show_candles(
instrument: str = "",
granularity: str = "D",
candlecount: int = 180,
additional_charts: Optional[Dict[str, bool]] = None,
external_axes: Optional[List[plt.Axes]] = None,
):
"""Show candle chart.
Note that additional plots (ta indicators) not supported in external axis mode.
Parameters
----------
instrument : str
The loaded currency pair
granularity : str, optional
The timeframe to get for the candle chart. Seconds: S5, S10, S15, S30
Minutes: M1, M2, M4, M5, M10, M15, M30 Hours: H1, H2, H3, H4, H6, H8, H12
Day (default): D, Week: W Month: M,
candlecount : int, optional
Limit for the number of data points
additional_charts : Dict[str, bool]
A dictionary of flags to include additional charts
external_axes : Optional[List[plt.Axes]], optional
External axes (2 axes are expected in the list), by default None
"""
df_candles = get_candles_dataframe(instrument, granularity, candlecount)
if (
df_candles is not False
and not df_candles.empty
and additional_charts is not None
):
plots_to_add, legends, subplot_legends = add_plots(
df_candles, additional_charts
)
else:
plots_to_add, legends, subplot_legends = None, [], []
candle_chart_kwargs = {
"type": "candle",
"style": theme.mpf_style,
"mav": (20, 50),
"volume": True,
"xrotation": theme.xticks_rotation,
"scale_padding": {"left": 0.3, "right": 1, "top": 0.8, "bottom": 0.8},
"update_width_config": {
"candle_linewidth": 0.6,
"candle_width": 0.8,
"volume_linewidth": 0.8,
"volume_width": 0.8,
},
"warn_too_much_data": 10000,
}
# This plot has 2 axes
if not external_axes:
candle_chart_kwargs["returnfig"] = True
candle_chart_kwargs["figratio"] = (10, 7)
candle_chart_kwargs["figscale"] = 1.10
candle_chart_kwargs["figsize"] = plot_autoscale()
if plots_to_add:
candle_chart_kwargs["addplot"] = plots_to_add
if isinstance(df_candles, pd.DataFrame):
fig, ax = mpf.plot(df_candles, **candle_chart_kwargs)
fig.suptitle(
f"{instrument} {granularity}",
x=0.055,
y=0.965,
horizontalalignment="left",
)
if len(legends) > 0:
ax[0].legend(legends)
# pylint: disable=C0200
for i in range(0, len(subplot_legends), 2):
ax[subplot_legends[i]].legend(subplot_legends[i + 1])
theme.visualize_output(force_tight_layout=False)
else:
logger.error("Data not found")
console.print("[red]Data not found[/red]\n")
elif is_valid_axes_count(external_axes, 2):
ax, volume = external_axes
candle_chart_kwargs["ax"] = ax
candle_chart_kwargs["volume"] = volume
mpf.plot(df_candles, **candle_chart_kwargs)
else:
return
@log_start_end(log=logger)
@check_api_key(["OANDA_ACCOUNT", "OANDA_TOKEN", "OANDA_ACCOUNT_TYPE"])
def calendar(instrument: str, days: int = 7):
"""View calendar of significant events.
Parameters
----------
instrument : str
The loaded currency pair
days : int
Number of days in advance
"""
df_calendar = get_calendar_request(days, instrument)
if df_calendar is not False and not df_calendar.empty:
console.print(df_calendar.to_string(index=False))
elif df_calendar is not False and df_calendar.empty:
console.print("No calendar records were found.\n")
else:
console.print("No data was returned from Oanda.\n")
# Utilities
@log_start_end(log=logger)
def add_plots(df: pd.DataFrame, additional_charts: Dict[str, bool]):
"""Add additional plots to the candle chart.
Parameters
----------
df : pd.DataFrame
The source data
additional_charts : Dict[str, bool]
A dictionary of flags to include additional charts
Returns
-------
Tuple
Tuple of lists containing the plots, legends and subplot legends
"""
panel_number = 2
plots_to_add = []
legends = []
subplot_legends = []
if additional_charts["ad"]:
ad = ta.ad(df["High"], df["Low"], df["Close"], df["Volume"])
ad_plot = mpf.make_addplot(ad, panel=panel_number)
plots_to_add.append(ad_plot)
subplot_legends.extend([panel_number * 2, ["AD"]])
panel_number += 1
if additional_charts["bbands"]:
bbands = ta.bbands(df["Close"])
bbands = bbands.drop("BBB_5_2.0", axis=1)
bbands_plot = mpf.make_addplot(bbands, panel=0)
plots_to_add.append(bbands_plot)
legends.extend(["Lower BBand", "Middle BBand", "Upper BBand"])
if additional_charts["cci"]:
cci = ta.cci(df["High"], df["Low"], df["Close"])
cci_plot = mpf.make_addplot(cci, panel=panel_number)
plots_to_add.append(cci_plot)
subplot_legends.extend([panel_number * 2, ["CCI"]])
panel_number += 1
if additional_charts["ema"]:
ema = ta.ema(df["Close"])
ema_plot = mpf.make_addplot(ema, panel=0)
plots_to_add.append(ema_plot)
legends.append("10 EMA")
if additional_charts["rsi"]:
rsi = ta.rsi(df["Close"])
rsi_plot = mpf.make_addplot(rsi, panel=panel_number)
plots_to_add.append(rsi_plot)
subplot_legends.extend([panel_number * 2, ["RSI"]])
panel_number += 1
if additional_charts["obv"]:
obv = ta.obv(df["Close"], df["Volume"])
obv_plot = mpf.make_addplot(obv, panel=panel_number)
plots_to_add.append(obv_plot)
subplot_legends.extend([panel_number * 2, ["OBV"]])
panel_number += 1
if additional_charts["sma"]:
sma_length = [20, 50]
for length in sma_length:
sma = ta.sma(df["Close"], length=length)
sma_plot = mpf.make_addplot(sma, panel=0)
plots_to_add.append(sma_plot)
legends.append(f"{length} SMA")
if additional_charts["vwap"]:
vwap = ta.vwap(df["High"], df["Low"], df["Close"], df["Volume"])
vwap_plot = mpf.make_addplot(vwap, panel=0)
plots_to_add.append(vwap_plot)
legends.append("vwap")
return plots_to_add, legends, subplot_legends
@log_start_end(log=logger)
def book_plot(
df: pd.DataFrame,
instrument: str,
book_type: str,
external_axes: Optional[List[plt.Axes]] = None,
):
"""Plot the order book for a given instrument.
Parameters
----------
df : pd.DataFrame
Order book data
instrument : str
The loaded currency pair
book_type : str
Order book type
external_axes : Optional[List[plt.Axes]], optional
External axes (1 axis is expected in the list), by default None
"""
df = df.apply(pd.to_numeric)
df["shortCountPercent"] = df["shortCountPercent"] * -1
axis_origin = max(
abs(max(df["longCountPercent"])), abs(max(df["shortCountPercent"]))
)
# This plot has 1 axis
if not external_axes:
_, ax = plt.subplots(figsize=plot_autoscale(), dpi=PLOT_DPI)
elif is_valid_axes_count(external_axes, 1):
(ax,) = external_axes
else:
return
ax.set_xlim(-axis_origin, +axis_origin)
sns.barplot(
x="longCountPercent",
y="price",
data=df,
label="Count Percent",
color=theme.up_color,
orient="h",
)
sns.barplot(
x="shortCountPercent",
y="price",
data=df,
label="Prices",
color=theme.down_color,
orient="h",
)
ax.invert_yaxis()
ax.yaxis.set_major_locator(mticker.MultipleLocator(10))
ax.set_xlabel("Count Percent")
ax.set_ylabel("Price")
ax.set_title(f"{instrument} {book_type}")
theme.style_primary_axis(ax)
if not external_axes:
theme.visualize_output()