Files
OpenBB/openbb_terminal/forex/forex_helper.py
Colin Delahunty 1ca7fd9beb Improves the settings/userdata command, and upgrade ruff (#5359)
* Began the fax

* Bumped ruff

* Fixed a BUNCH of ruff errors

* Fixed a lot of ruff errors

* Simplify if statements

* Fixed black

* Updated some stuff

* Updated some stuff

* A lot more ruff fixes

* Added a fix

* Added a fix

* Fixed mypy

* Fixed some stuff

* Reverted one thing

* Small fix

* Small fix

* Small fix

* Added fix

* Added fix

* Update requirements
2023-08-22 20:11:48 +00:00

302 lines
8.4 KiB
Python

"""Forex helper."""
import argparse
import logging
import os
import re
from datetime import datetime, timedelta
from typing import Dict, Iterable, List, Optional
import pandas as pd
import yfinance as yf
from openbb_terminal.core.plots.plotly_ta.ta_class import PlotlyTA
from openbb_terminal.decorators import log_start_end
from openbb_terminal.forex import av_model, polygon_model
from openbb_terminal.rich_config import console
from openbb_terminal.stocks import stocks_helper
CANDLE_SORT = [
"adjclose",
"open",
"close",
"high",
"low",
"volume",
"logret",
]
FOREX_SOURCES: Dict = {
"YahooFinance": "YahooFinance",
"AlphaVantage": "AlphaAdvantage",
"Polygon": "Polygon",
"Oanda": "Oanda",
}
SOURCES_INTERVALS: Dict = {
"YahooFinance": [
"1min",
"5min",
"15min",
"30min",
"60min",
"90min",
"1hour",
"1day",
"5day",
"1week",
"1month",
"3month",
],
"AlphaVantage": ["1min", "5min", "15min", "30min", "60min"],
}
INTERVAL_MAPS: Dict = {
"YahooFinance": {
"1min": "1m",
"2min": "2m",
"5min": "5m",
"15min": "15m",
"30min": "30m",
"60min": "60m",
"90min": "90m",
"1hour": "60m",
"1day": "1d",
"5day": "5d",
"1week": "1wk",
"1month": "1mo",
"3month": "3mo",
},
"AlphaVantage": {
"1min": 1,
"5min": 5,
"15min": 15,
"30min": 30,
"60min": 60,
"1day": 1,
},
}
logger = logging.getLogger(__name__)
@log_start_end(log=logger)
def load(
to_symbol: str,
from_symbol: str,
resolution: str = "d",
interval: str = "1day",
start_date: Optional[str] = None,
end_date: Optional[str] = None,
source: str = "YahooFinance",
verbose: bool = False,
) -> pd.DataFrame:
"""Load forex for two given symbols.
Parameters
----------
to_symbol : str
The from currency symbol. Ex: USD, EUR, GBP, YEN
from_symbol : str
The from currency symbol. Ex: USD, EUR, GBP, YEN
resolution : str, optional
The resolution for the data, by default "d"
interval : str, optional
What interval to get data for, by default "1day"
start_date : Optional[str], optional
When to begin loading in data, by default last_year.strftime("%Y-%m-%d")
end_date : Optional[str], optional
When to end loading in data, by default None
source : str, optional
Where to get data from, by default "YahooFinance"
verbose : bool, optional
Display verbose information on what was the pair that was loaded, by default True
Returns
-------
pd.DataFrame
The loaded data
Examples
--------
>>> from openbb_terminal.sdk import openbb
>>> openbb.forex.load(from_symbol="EUR", to_symbol="USD", start_date="2020-11-30", end_date="2022-12-01")
"""
if start_date is None:
start_date = (datetime.now() - timedelta(days=365)).strftime("%Y-%m-%d")
if end_date is None:
end_date = (datetime.now() + timedelta(days=1)).strftime("%Y-%m-%d")
else:
end_date = (
datetime.strptime(end_date, "%Y-%m-%d") + timedelta(days=1)
).strftime("%Y-%m-%d")
if source in ["YahooFinance", "AlphaVantage"]:
interval_map = INTERVAL_MAPS[source]
if interval not in interval_map and resolution != "d":
if verbose:
console.print(
f"Interval not supported by {FOREX_SOURCES[source]}."
" Need to be one of the following options",
list(interval_map.keys()),
)
return pd.DataFrame()
# Check interval in multiple ways
if interval in interval_map:
clean_interval = interval_map[interval]
elif interval in interval_map.values():
clean_interval = interval
else:
console.print(f"[red]'{interval}' is an invalid interval[/red]\n")
return pd.DataFrame()
if source == "AlphaVantage":
if "min" in interval:
resolution = "i"
df = av_model.get_historical(
to_symbol=to_symbol,
from_symbol=from_symbol,
resolution=resolution,
interval=clean_interval,
start_date=start_date,
end_date=end_date,
)
df.index.name = "date"
df.name = f"{from_symbol}/{to_symbol}"
return df
if source == "YahooFinance":
df = yf.download(
f"{from_symbol}{to_symbol}=X",
start=datetime.strptime(start_date, "%Y-%m-%d"),
end=datetime.strptime(end_date, "%Y-%m-%d"),
interval=clean_interval,
progress=verbose,
)
df.index = pd.to_datetime(df.index).tz_localize(None)
df.index.name = "date"
df.name = f"{from_symbol}/{to_symbol}"
return df
if source == "Polygon":
# Interval for polygon gets broken into multiplier and timeframe
temp = re.split(r"(\d+)", interval)
multiplier = int(temp[1])
timeframe = temp[2]
if timeframe == "min":
timeframe = "minute"
df = polygon_model.get_historical(
f"{from_symbol}{to_symbol}",
multiplier=multiplier,
timespan=timeframe,
start_date=start_date,
end_date=end_date,
)
df.index.name = "date"
df.name = f"{from_symbol}/{to_symbol}"
return df
console.print(f"Source {source} not supported")
return pd.DataFrame()
@log_start_end(log=logger)
def get_yf_currency_list() -> List:
"""Load YF list of forex pair a local file."""
path = os.path.join(os.path.dirname(__file__), "data/yahoofinance_forex.json")
return sorted(list(set(pd.read_json(path)["from_symbol"])))
YF_CURRENCY_LIST = get_yf_currency_list()
@log_start_end(log=logger)
def check_valid_yf_forex_currency(fx_symbol: str) -> str:
"""Check if given symbol is supported on Yahoo Finance.
Parameters
----------
fx_symbol : str
Symbol to check
Returns
-------
str
Currency symbol
Raises
------
argparse.ArgumentTypeError
Symbol not valid on YahooFinance
"""
if fx_symbol.upper() in get_yf_currency_list():
return fx_symbol.upper()
raise argparse.ArgumentTypeError(
f"{fx_symbol.upper()} not found in YahooFinance supported currency codes. "
)
@log_start_end(log=logger)
def display_candle(
data: pd.DataFrame,
to_symbol: str = "",
from_symbol: str = "",
ma: Optional[Iterable[int]] = None,
external_axes: bool = False,
add_trend: bool = False,
yscale: str = "linear",
):
"""Show candle plot for fx data.
Parameters
----------
data : pd.DataFrame
Loaded fx historical data
to_symbol : str
To forex symbol
from_symbol : str
From forex symbol
ma : Optional[Iterable[int]]
Moving averages
external_axes : bool, optional
Whether to return the figure object or not, by default False
"""
# We check if there's Volume data to avoid errors and empty subplots
if not (has_volume := False) and "Volume" in data.columns:
has_volume = bool(data["Volume"].sum() > 0)
if add_trend and (data.index[1] - data.index[0]).total_seconds() >= 86400:
if "date_id" not in data.columns:
data = stocks_helper.process_candle(data)
data = stocks_helper.find_trendline(data, "OC_High", "high")
data = stocks_helper.find_trendline(data, "OC_Low", "low")
indicators = {}
if ma:
indicators = dict(rma=dict(length=ma))
data.name = f"{from_symbol}/{to_symbol}"
fig = PlotlyTA.plot(data, indicators, volume=has_volume)
if add_trend:
fig.add_trend(data, secondary_y=False)
fig.update_yaxes(type=yscale, row=1, col=1, nticks=20, secondary_y=False)
return fig.show(external=external_axes)
@log_start_end(log=logger)
def parse_forex_symbol(input_symbol):
"""Parse potential forex symbols."""
for potential_split in ["-", "/"]:
if potential_split in input_symbol:
symbol = input_symbol.replace(potential_split, "")
return symbol
if len(input_symbol) != 6:
raise argparse.ArgumentTypeError("Input symbol should be 6 characters.\n ")
return input_symbol.upper()