diff --git a/gamestonk_terminal/due_diligence/reddit_view.py b/gamestonk_terminal/due_diligence/reddit_view.py index 5c536add200..3348e8638eb 100644 --- a/gamestonk_terminal/due_diligence/reddit_view.py +++ b/gamestonk_terminal/due_diligence/reddit_view.py @@ -85,6 +85,10 @@ def due_diligence(other_args: List[str], ticker: str): "News", "Advice", "Chart", + "Charts and Setups", + "Fundamental Analysis", + "forex", + "Trade Idea", ] l_sub_reddits = [ "pennystocks", @@ -94,6 +98,8 @@ def due_diligence(other_args: List[str], ticker: str): "stocks", "investing", "wallstreetbets", + "forex", + "Forexstrategy", ] submissions = psaw_api.search_submissions( diff --git a/gamestonk_terminal/forex/README.md b/gamestonk_terminal/forex/README.md index 5c27d28a4fd..5e127cee0a3 100644 --- a/gamestonk_terminal/forex/README.md +++ b/gamestonk_terminal/forex/README.md @@ -30,6 +30,14 @@ This menu utilizes oanda to enable support for trading forex. * Display the orderbook if Oanda provides one for the forex instrument * [positionbook](#positionbook) * Display the positionbook if Oanda provides one for the forex instrument +* [news](#news) + * Latest news of the currency provided by [News API] +* [reddit](#reddit) + * Gets due diligence from another user's post [Reddit] +* [eda](#eda) + * This menu aims to perform exploratory data analysis on a pre-loaded forex instrument. +* [ba](#ba) + * This menu aims to extrapolate behavioural analysis regarding a pre-loaded forex instrument. ## summary ```text @@ -108,13 +116,21 @@ Specify the instrument to use in the format EUR_USD ## candles ```text -usage: candles [-g GRANULARITY] [-c CANDLECOUNT] +usage: candles [-g GRANULARITY] [-c CANDLECOUNT] [-abcCeorsv] ``` Retrieve a candlestick chart for the currently loaded instrument. Not a live chart. * -g : Specify the granularity, which is the timeframe each candle represents. The available granularities are Seconds: S5, S10, S15, S30 Minutes: M1, M2, M4, M5, M10, M15, M30 Hours: H1, H2, H3, H4, H6, H8, H12 Day: D, Week: W Month: M. (default=D) * -c : Specify the number of candles to retrieve. (default=180) +* -a : Adds AD (Accumulation/Distribution Index) to the chart. AD is a cumulative indicator that uses volume and price to assess whether a stock is being accumulated or distributed. This provides insight into how strong a trend is. [Investopedia](#https://www.investopedia.com/terms/a/accumulationdistribution.asp) +* -b : Adds Bollinger Bands to the chart. Bollinger bands are a set of trendlines plotted two standard deviations (positively and negatively) away from a simple moving average of a security's price. [Investopedia](#https://www.investopedia.com/terms/b/bollingerbands.asp) +* -C : Adds CCI (Commodity Channel Index) to the chart. CCI is a momentum-based oscillator used to help determine when an investment vehicle is reaching a condition of being overbought or oversold. (default=14) [Investopedia](#https://www.investopedia.com/terms/c/commoditychannelindex.asp) +* -e : Adds EMA (Exponential Moving Average) to the chart. EMA is a moving average that places a greater weight and significance on the most recent data points. (default=10) [Investopedia](#https://www.investopedia.com/terms/e/ema.asp) +* -o : Adds OBV (On Balance Volume) to the chart. OBV is a momentum indicator that uses volume flow to predict changes in price. [Investopedia](#https://www.investopedia.com/terms/o/onbalancevolume.asp) +* -r : Adds RSI (Relative Strength Index) to the chart. RSI is a momentum indicator that measures the magnitude of recent price changes to evaluate overbought or oversold conditions. (default=14) [Investopedia](#https://www.investopedia.com/terms/r/rsi.asp) +* -s : Adds SMA (Simple Moving Average) to the chart. SMA calculates the average of a selected range of prices, usually closing prices, by the number of periods in that range. (defaults= 20, 50) [Investopedia](#https://www.investopedia.com/terms/s/sma.asp) +* -v : Adds VWAP (Volume Weighted Average Price) to the chart. VWAP gives the average price a security has traded at throughout the day, based on both volume and price. [Investopedia](#https://www.investopedia.com/terms/v/vwap.asp) ## price ```text @@ -144,3 +160,38 @@ Plots the current orderbook for loaded instrument if one is provided by Oanda. N usage: positionbook ``` Plots the current positionbook for the loaded instrument if one is provided by Oanda. Not a live chart. + +## news + +```text +news [-n N_NUM] +``` + +Prints latest news about currency, including date, title and web link. [Source: News API] + +* -n : Number of latest news being printed. Default 10. + +## reddit + +```text +usage: red [-l N_LIMIT] [-d N_DAYS] [-a] +``` + +Print top stock's due diligence from other users. [Source: Reddit] + +* -l : limit of posts to retrieve +* -d : number of prior days to look for +* -a : "search through all flairs (apart from Yolo and Meme). Default False (i.e. use flairs: DD, technical analysis, Catalyst, News, Advice, Chart, Charts and Setups, Fundamental Analysis, Forex, Trade Idea) + +## eda +```text +usage: eda +``` + +Access the exploratory data analysis menu. + +## ba +```text +usage: ba +``` +Access the behavioural analysis menu. diff --git a/gamestonk_terminal/forex/fx_controller.py b/gamestonk_terminal/forex/fx_controller.py index b1ab4dc7c68..b796f83ff25 100644 --- a/gamestonk_terminal/forex/fx_controller.py +++ b/gamestonk_terminal/forex/fx_controller.py @@ -6,6 +6,11 @@ from gamestonk_terminal import feature_flags as gtff from gamestonk_terminal.menu import session from gamestonk_terminal.forex import fx_view from gamestonk_terminal import config_terminal as cfg +from gamestonk_terminal.due_diligence import news_view, reddit_view +from gamestonk_terminal.behavioural_analysis import ba_controller +from gamestonk_terminal.exploratory_data_analysis import eda_controller +import pandas as pd + account = cfg.OANDA_ACCOUNT @@ -31,6 +36,10 @@ class ForexController: "candles", "pending", "calendar", + "news", + "reddit", + "eda", + "ba", ] def __init__(self): @@ -69,6 +78,16 @@ class ForexController: print(" order place limit order -u # of units -p price") print(" orderbook print orderbook") print(" positionbook print positionbook") + print(" news print news [News API]") + print( + " reddit search reddit for posts about the loaded instrument" + ) + print( + " eda > exploratory data analysis, e.g.: decompose, cusum, residuals analysis" + ) + print( + " ba > behavioural analysis, from: reddit, stocktwits, twitter, google" + ) print("") def switch(self, an_input: str): @@ -152,6 +171,28 @@ class ForexController: """Call calendar""" fx_view.calendar(self.instrument, other_args) + def call_news(self, other_args: List[str]): + """Call news [News API]""" + news_view.news(other_args, self.instrument) + + def call_reddit(self, other_args: List[str]): + instrument = fx_view.format_instrument(self.instrument, " ") + reddit_view.due_diligence(other_args, instrument) + + def call_eda(self, other_args): + df = fx_view.get_candles_dataframe(account, self.instrument, None) + df = df.rename(columns={"Close": "5. adjusted close"}) + instrument = self.instrument + s_start = pd.to_datetime(df.index.values[0]) + s_interval = "1440min" + eda_controller.menu(df, instrument, s_start, s_interval) + + def call_ba(self, other_args: List[str]): + instrument = fx_view.format_instrument(self.instrument, " ") + df = fx_view.get_candles_dataframe(account, self.instrument, None) + s_start = pd.to_datetime(df.index.values[0]) + ba_controller.menu(instrument, s_start) + def menu(): """Oanda Menu""" diff --git a/gamestonk_terminal/forex/fx_view.py b/gamestonk_terminal/forex/fx_view.py index cc4f1c59842..0ac34599671 100644 --- a/gamestonk_terminal/forex/fx_view.py +++ b/gamestonk_terminal/forex/fx_view.py @@ -12,8 +12,13 @@ from oandapyV20.exceptions import V20Error from gamestonk_terminal import config_terminal as cfg from gamestonk_terminal import config_plot as cfgPlot from gamestonk_terminal import feature_flags as gtff -from gamestonk_terminal.helper_funcs import parse_known_args_and_warn, plot_autoscale +from gamestonk_terminal.helper_funcs import ( + parse_known_args_and_warn, + plot_autoscale, + check_non_negative, +) import pandas as pd +import pandas_ta as ta import mplfinance as mpf import matplotlib.pyplot as plt import matplotlib.ticker as mticker @@ -236,6 +241,11 @@ def create_order(accountID, instrument, other_args: List[str]): ns_parser = parse_known_args_and_warn(parser, other_args) if not ns_parser: return + if "JPY" or "THB" or "HUF" in instrument: + ns_parser.price = round(ns_parser.price, 3) + else: + ns_parser.price = round(ns_parser.price, 5) + check_non_negative(ns_parser.price) data = { "order": { @@ -250,37 +260,25 @@ def create_order(accountID, instrument, other_args: List[str]): try: request = orders.OrderCreate(accountID, data) response = client.request(request) + order_data = [] + order_data.append( + { + "Order ID": response["orderCreateTransaction"]["id"], + "Instrument": response["orderCreateTransaction"]["instrument"], + "Price": response["orderCreateTransaction"]["price"], + "Units": response["orderCreateTransaction"]["units"], + } + ) - if "orderFillTransaction" in response["orderCreateTransaction"]: - order_id = response["orderCreateTransaction"]["orderFillTransaction"]["id"] - order_instrument = response["orderCreateTransaction"][ - "orderFillTransaction" - ]["instrument"] - units = response["orderCreateTransaction"]["orderFillTransaction"]["units"] - price = response["orderCreateTransaction"]["orderFillTransaction"]["price"] - print("Order Filled:") - print(f"ID: {order_id}") - print(f"Instrument: {order_instrument}") - print(f"Units: {units}") - print(f"Price: {price}") - print("") - else: - order_creation_id = response["orderCreateTransaction"]["id"] - order_instrument = response["orderCreateTransaction"]["instrument"] - units = response["orderCreateTransaction"]["units"] - price = response["orderCreateTransaction"]["price"] - print("Order created:") - print(f"ID: {order_creation_id}") - print(f"Instrument: {order_instrument}") - print(f"Units: {units}") - print(f"Price: {price}") - print("") + df = pd.DataFrame.from_dict(order_data) + print(df.to_string(index=False)) + print("") - except Exception as e: - print(e) except V20Error as e: d_error = eval(e.msg) - print(d_error["message"], "\n") + print(d_error["errorMessage"], "\n") + except Exception as e: + print(e) def cancel_pending_order(accountID, other_args: List[str]): @@ -327,21 +325,28 @@ def get_pending_orders(accountID, other_args): try: request = orders.OrdersPending(accountID) response = client.request(request) + pending_data = [] for i in range(len(response["orders"])): - order_id = response["orders"][i]["id"] - instrument = response["orders"][i]["instrument"] - price = response["orders"][i]["price"] - units = response["orders"][i]["units"] - create_time = response["orders"][i]["createTime"] - time_in_force = response["orders"][i]["timeInForce"] - print(f"Order ID: {order_id}") - print(f"Instrument: {instrument}") - print(f"Price: {price}") - print(f"Units: {units}") - print(f"Time created: {create_time}") - print(f"Time in force: {time_in_force}") - print("-" * 30) + pending_data.append( + { + "Order ID": response["orders"][i]["id"], + "Instrument": response["orders"][i]["instrument"], + "Price": response["orders"][i]["price"], + "Units": response["orders"][i]["units"], + "Time Created": response["orders"][i]["createTime"][:10] + + " " + + response["orders"][i]["createTime"][11:19], + "Time In Force": response["orders"][i]["timeInForce"], + } + ) + + df = pd.DataFrame.from_dict(pending_data) + if not pending_data: + print("No pending orders") + else: + print(df.to_string(index=False)) print("") + except V20Error as e: d_error = eval(e.msg) print(d_error["errorMessage"], "\n") @@ -359,22 +364,26 @@ def get_open_positions(accountID, other_args): try: request = positions.OpenPositions(accountID) response = client.request(request) + position_data = [] for i in range(len(response["positions"])): - instrument = response["positions"][i]["instrument"] - long_units = response["positions"][i]["long"]["units"] - long_pl = response["positions"][i]["long"]["pl"] - long_upl = response["positions"][i]["long"]["unrealizedPL"] - short_units = response["positions"][i]["short"]["units"] - short_pl = response["positions"][i]["short"]["pl"] - short_upl = response["positions"][i]["short"]["unrealizedPL"] - print(f"Instrument: {instrument}\n") - print(f"Long Units: {long_units}") - print(f"Total Long P/L: {long_pl}") - print(f"Long Unrealized P/L: {long_upl}\n") - print(f"Short Units: {short_units}") - print(f"Total Short P/L: {short_pl}") - print(f"Short Unrealized P/L: {short_upl}") - print("-" * 30 + "\n") + position_data.append( + { + "Instrument": response["positions"][i]["instrument"], + "Long Units": response["positions"][i]["long"]["units"], + "Total Long P/L": response["positions"][i]["long"]["units"], + "Unrealized Long P/L": response["positions"][i]["long"][ + "unrealizedPL" + ], + "Short Units": response["positions"][i]["short"]["units"], + "Total Short P/L": response["positions"][i]["short"]["pl"], + "Short Unrealized P/L": response["positions"][i]["short"][ + "unrealizedPL" + ], + } + ) + + df = pd.DataFrame.from_dict(position_data) + print(df.to_string(index=False)) print("") except V20Error as e: @@ -417,8 +426,10 @@ def get_open_trades(accountID, other_args): "unrealizedPL": "Unrealized P/L", } ) - print(df) + + print(df.to_string(index=False)) print("") + except KeyError: print("No trades were found") print("") @@ -467,24 +478,25 @@ def close_trade(accountID, other_args: List[str]): request = trades.TradeClose(accountID, ns_parser.orderID, data) response = client.request(request) - order_id = response["orderCreateTransaction"]["tradeClose"]["tradeID"] - order_instrument = response["orderFillTransaction"]["instrument"] - units = response["orderCreateTransaction"]["units"] - price = response["orderFillTransaction"]["price"] - pl = response["orderFillTransaction"]["pl"] - print("Order closed:") - print(f"ID: {order_id}") - print(f"Instrument: {order_instrument}") - print(f"Units: {units}") - print(f"Price: {price}") - print(f"P/L: {pl}") + close_data = [] + close_data.append( + { + "OrderID": response["orderCreateTransaction"]["tradeClose"]["tradeID"], + "Instrument": response["orderFillTransaction"]["instrument"], + "Units": response["orderCreateTransaction"]["units"], + "Price": response["orderFillTransaction"]["price"], + "P/L": response["orderFillTransaction"]["pl"], + } + ) + df = pd.DataFrame.from_dict(close_data) + print(df.to_string(index=False)) print("") - except Exception as e: - print(e, "\n") except V20Error as e: d_error = eval(e.msg) print(d_error["errorMessage"], "\n") + except Exception as e: + print(e, "\n") def show_candles(accountID, instrument, other_args: List[str]): @@ -515,71 +527,164 @@ def show_candles(accountID, instrument, other_args: List[str]): required=False, help="The number of candles to retrieve. Default:180 ", ) + parser.add_argument( + "-a", + "--ad", + dest="ad", + action="store_true", + help="Adds ad (Accumulation/Distribution Indicator) to the chart", + ) + parser.add_argument( + "-b", + "--bbands", + dest="bbands", + action="store_true", + help="Adds Bollinger Bands to the chart", + ) + parser.add_argument( + "-C", + "--cci", + dest="cci", + action="store_true", + help="Adds cci (Commodity Channel Index) to the chart", + ) + parser.add_argument( + "-e", + "--ema", + dest="ema", + action="store_true", + help="Adds ema (Exponential Moving Average) to the chart", + ) + parser.add_argument( + "-o", + "--obv", + dest="obv", + action="store_true", + help="Adds obv (On Balance Volume) to the chart", + ) + parser.add_argument( + "-r", + "--rsi", + dest="rsi", + action="store_true", + help="Adds rsi (Relative Strength Index) to the chart", + ) + parser.add_argument( + "-s", + "--sma", + dest="sma", + action="store_true", + help="Adds sma (Simple Moving Average) to the chart", + ) + parser.add_argument( + "-v", + "--vwap", + dest="vwap", + action="store_true", + help="Adds vwap (Volume Weighted Average Price) to the chart", + ) ns_parser = parse_known_args_and_warn(parser, other_args) if not ns_parser: return parameters = {} - parameters["granularity"] = ns_parser.granularity + parameters["granularity"] = ns_parser.granularity.upper() parameters["count"] = ns_parser.candlecount try: instrument = format_instrument(instrument, "_") - request = instruments.InstrumentsCandles(instrument, params=parameters) - response = client.request(request) - process_candle_response(response) - oanda_fix_date(".temp_candles.csv") - df = pd.read_csv(".candles.csv", index_col=0) - df.index = pd.to_datetime(df.index) - df.columns = ["Open", "High", "Low", "Close", "Volume"] + df = get_candles_dataframe(accountID, instrument, parameters) + + plots_to_add, legends, subplot_legends = add_plots(df, ns_parser) + if gtff.USE_ION: plt.ion() - mpf.plot( + + fig, ax = mpf.plot( df, type="candle", style="charles", volume=True, title=f"{instrument} {ns_parser.granularity}", + returnfig=True, + addplot=plots_to_add, ) + + ax[0].set_title(f"{instrument} {ns_parser.granularity}") + ax[0].legend(legends) + for i in range(0, len(subplot_legends), 2): + ax[subplot_legends[i]].legend(subplot_legends[i + 1]) + print("") except V20Error as e: d_error = eval(e.msg) print(d_error["errorMessage"], "\n") + except TypeError as e: + print(e) -def process_candle_response(response): - with open(".temp_candles.csv", "w") as out: - for i in range(len(response["candles"])): - time = response["candles"][i]["time"] - volume = response["candles"][i]["volume"] - o = response["candles"][i]["mid"]["o"] - h = response["candles"][i]["mid"]["h"] - low = response["candles"][i]["mid"]["l"] - c = response["candles"][i]["mid"]["c"] - out.write( - str(time) - + "," - + str(o) - + "," - + str(h) - + "," - + str(low) - + "," - + str(c) - + "," - + str(volume) - + "\n" - ) +def add_plots(df, ns_parser): + panel_number = 2 + plots_to_add = [] + legends = [] + subplot_legends = [] + if ns_parser.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 -def oanda_fix_date(file): - with open(file) as candle_file: - lines = candle_file.readlines() - with open(".candles.csv", "w") as out: - out.write("Datetime, Open, High, Low, Close, Volume\n") - for line in lines: - with open(".candles.csv", "a") as output: - output.write(line[:10] + " " + line[11:19] + line[30:]) + if ns_parser.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 ns_parser.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 ns_parser.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 ns_parser.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 ns_parser.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 ns_parser.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 ns_parser.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 def calendar(instrument, other_args: List[str]): @@ -665,6 +770,38 @@ def calendar(instrument, other_args: List[str]): print(d_error["message"], "\n") +def get_candles_dataframe(accountID, instrument, parameters): + if not parameters: + parameters = { + "granularity": "D", + "count": "1500", + } + try: + request = instruments.InstrumentsCandles(instrument, params=parameters) + response = client.request(request) + candles_data = [] + for i in range(len(response["candles"])): + candles_data.append( + { + "Date": response["candles"][i]["time"][:10] + + " " + + response["candles"][i]["time"][11:19], + "Open": float(response["candles"][i]["mid"]["o"]), + "High": float(response["candles"][i]["mid"]["h"]), + "Low": float(response["candles"][i]["mid"]["l"]), + "Close": float(response["candles"][i]["mid"]["c"]), + "Volume": response["candles"][i]["volume"], + } + ) + df = pd.DataFrame(candles_data) + df.set_index("Date", inplace=True) + df.index = pd.to_datetime(df.index) + return df + except V20Error as e: + d_error = eval(e.msg) + print(d_error["errorMessage"], "\n") + + def load(other_args: List[str]): """Load a forex instrument to use""" parser = argparse.ArgumentParser( @@ -740,9 +877,16 @@ def book_plot(df, instrument, book_type): def format_instrument(instrument, char): - if char not in instrument: - instrument_list = list(instrument) - instrument_list.pop(3) - instrument_list.insert(3, char) - instrument = "".join(map(str, instrument_list)) - return instrument + try: + if char not in instrument: + instrument_list = list(instrument) + instrument_list.pop(3) + if char == " ": + instrument = "".join(map(str, instrument_list)) + else: + instrument_list.insert(3, char) + instrument = "".join(map(str, instrument_list)) + return instrument + except TypeError: + print("Please load an instrument") + print("")