From 7af06fcd697ba26eb8946575519d52353f37d363 Mon Sep 17 00:00:00 2001 From: Lev Gusiev Date: Fri, 19 Jun 2026 23:55:03 +0200 Subject: [PATCH 01/12] feat(#33): add func to get data from yfinance --- .gitignore | 3 ++- src/argus/clients/yfinancy_client.py | 0 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 src/argus/clients/yfinancy_client.py diff --git a/.gitignore b/.gitignore index 9a1d8a1..973c350 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,5 @@ __pycache__ .ruff_cache # Virtual environment -.env \ No newline at end of file +.env +.venv \ No newline at end of file diff --git a/src/argus/clients/yfinancy_client.py b/src/argus/clients/yfinancy_client.py new file mode 100644 index 0000000..e69de29 From cc808f4cd1b6180aca5b337b618db0bdaac7077d Mon Sep 17 00:00:00 2001 From: Lev Gusiev Date: Sat, 20 Jun 2026 00:01:54 +0200 Subject: [PATCH 02/12] feat(#33): normalize the dataframe from yfinance --- src/argus/clients/yfinancy_client.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/argus/clients/yfinancy_client.py b/src/argus/clients/yfinancy_client.py index e69de29..206a5e3 100644 --- a/src/argus/clients/yfinancy_client.py +++ b/src/argus/clients/yfinancy_client.py @@ -0,0 +1,11 @@ +import yfinance as yf + +def get_timeseries(curr_symbol,start,end,interval): + data = yf.download(tickers=curr_symbol, + start=start,end=end,interval=interval + ) + # Need to figure out, how to normalize the dataframe + result = data.reset_index(level=2,col_level=2,drop=True) + result = result[["Date", "Close"]] + result = result.rename(columns={"Date": "date", "Close": "rate"}) + return result \ No newline at end of file From 54521bfc7bb5d150c928544e96567bb8997faa76 Mon Sep 17 00:00:00 2001 From: Lev Gusiev Date: Sat, 20 Jun 2026 01:19:00 +0200 Subject: [PATCH 03/12] feat(#33): replace mock-client --- src/argus/analytics/charts/trend_chart.py | 6 ++--- src/argus/clients/yfinancy_client.py | 19 +++++++++++----- src/argus/gui/app.py | 27 +++++------------------ src/argus/services/timeseries_service.py | 15 +++++-------- 4 files changed, 28 insertions(+), 39 deletions(-) diff --git a/src/argus/analytics/charts/trend_chart.py b/src/argus/analytics/charts/trend_chart.py index d475bc7..27dd211 100644 --- a/src/argus/analytics/charts/trend_chart.py +++ b/src/argus/analytics/charts/trend_chart.py @@ -3,7 +3,7 @@ from argus.services.timeseries_service import prepare_trend_analysis -def create_trendchart(curr: str, dates: pd.DataFrame): +def create_trendchart(curr_symbol: str, start:str,end:str,interval:str): """ Create a trend chart for exchange-rate analysis. @@ -31,8 +31,8 @@ def create_trendchart(curr: str, dates: pd.DataFrame): Minimum and maximum exchange-rate values are marked with scatter points and annotations. """ - df = pd.DataFrame() - df, min_max_rates = prepare_trend_analysis(curr, dates) + + df, min_max_rates = prepare_trend_analysis(curr_symbol,start,end,interval) min_date = min_max_rates["min_date"][0] min_rate = min_max_rates["min_rate"][0] max_date = min_max_rates["max_date"][0] diff --git a/src/argus/clients/yfinancy_client.py b/src/argus/clients/yfinancy_client.py index 206a5e3..31877ca 100644 --- a/src/argus/clients/yfinancy_client.py +++ b/src/argus/clients/yfinancy_client.py @@ -2,10 +2,19 @@ def get_timeseries(curr_symbol,start,end,interval): data = yf.download(tickers=curr_symbol, - start=start,end=end,interval=interval - ) + start=start,end=end,interval=interval,multi_level_index=False,progress=False + ) + if data is None: + return None + data = data.reset_index() + data = data[["Date","Close"]] + data = data.rename(columns={"Date": "date", "Close": "rate"}) + print(data.head()) + print(data.index) + print(data.columns) # Need to figure out, how to normalize the dataframe - result = data.reset_index(level=2,col_level=2,drop=True) - result = result[["Date", "Close"]] - result = result.rename(columns={"Date": "date", "Close": "rate"}) + #result = data.reset_index(level=2,col_level=2,drop=True) + #result = result[["Date", "Close"]] + #result = result.rename(columns={"Date": "date", "Close": "rate"}) + result = data.copy() return result \ No newline at end of file diff --git a/src/argus/gui/app.py b/src/argus/gui/app.py index 829a905..c656154 100644 --- a/src/argus/gui/app.py +++ b/src/argus/gui/app.py @@ -77,27 +77,10 @@ def show_trend() -> None: global trend_canvas global trend_chart_widget - mock_dates = { - "date": [ - "2026-06-01", - "2026-06-02", - "2026-06-03", - "2026-06-04", - "2026-06-05", - "2026-06-06", - "2026-06-07", - "2026-06-08", - "2026-06-09", - "2026-06-10", - "2026-06-11", - "2026-06-12", - "2026-06-13", - "2026-06-14", - "2026-06-15", - ] - } - mock_dates = pd.DataFrame(mock_dates) - mock_curr = "USD" + curr_symbol = "EURUSD=X", + start="2024-01-01", + end="2024-02-01", + interval="1d" calc_frame.pack_forget() conv_frame.pack_forget() @@ -108,7 +91,7 @@ def show_trend() -> None: content.pack(side="top", fill=tk.BOTH, expand=True) if trend_canvas is None: - fig = create_trendchart(mock_curr, mock_dates) + fig = create_trendchart(curr_symbol,start,end,interval) fig.set_size_inches(7, 4) trend_canvas = FigureCanvasTkAgg(fig, master=content) diff --git a/src/argus/services/timeseries_service.py b/src/argus/services/timeseries_service.py index 3646200..b506162 100644 --- a/src/argus/services/timeseries_service.py +++ b/src/argus/services/timeseries_service.py @@ -1,5 +1,5 @@ import pandas as pd -from argus.clients.mock_client import get_mock_timeseries +from argus.clients.yfinancy_client import get_timeseries from argus.analytics.metrics.trend_metrics import ( add_rolling_average, add_daily_percentage_change, @@ -7,9 +7,7 @@ ) -def prepare_trend_analysis( - mock_curr: str, df: pd.DataFrame -) -> tuple[pd.DataFrame, dict]: +def prepare_trend_analysis(curr_symbol: str, start:str,end:str,intervall:str) -> tuple[pd.DataFrame, dict] | None: """ Prepares the data for trend analysis by adding conversion rates, daily percentage change, and rolling average. @@ -21,11 +19,10 @@ def prepare_trend_analysis( Return: tuple[pd.DataFrame, dict] - a tuple containing the updated DataFrame with conversion rates, daily percentage change, and rolling average, and a dictionary with the minimum and maximum rates """ - df["rate"] = 0.0 - # For each date one API call to get the rate - for i in range(len(df)): - date = str(df.loc[i, "date"]) - df.loc[i, "rate"] = get_mock_timeseries(mock_curr, date) + + df = get_timeseries(curr_symbol, start,end,intervall) + if df is None: + return None df = add_daily_percentage_change(df) df = add_rolling_average(df) min_max_rates = get_min_max_rates(df) From 1943f5a6c92d0f8a041b5731058f041693c4c064 Mon Sep 17 00:00:00 2001 From: Lev Gusiev Date: Sat, 20 Jun 2026 01:33:12 +0200 Subject: [PATCH 04/12] feat(#33): remove debug prints --- src/argus/analytics/charts/trend_chart.py | 6 ++++-- src/argus/clients/yfinancy_client.py | 3 --- src/argus/gui/app.py | 10 ++++++---- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/argus/analytics/charts/trend_chart.py b/src/argus/analytics/charts/trend_chart.py index 27dd211..f889b83 100644 --- a/src/argus/analytics/charts/trend_chart.py +++ b/src/argus/analytics/charts/trend_chart.py @@ -31,8 +31,10 @@ def create_trendchart(curr_symbol: str, start:str,end:str,interval:str): Minimum and maximum exchange-rate values are marked with scatter points and annotations. """ - - df, min_max_rates = prepare_trend_analysis(curr_symbol,start,end,interval) + result = prepare_trend_analysis(curr_symbol,start,end,interval) + if result is None: + return None + df, min_max_rates = result min_date = min_max_rates["min_date"][0] min_rate = min_max_rates["min_rate"][0] max_date = min_max_rates["max_date"][0] diff --git a/src/argus/clients/yfinancy_client.py b/src/argus/clients/yfinancy_client.py index 31877ca..170289e 100644 --- a/src/argus/clients/yfinancy_client.py +++ b/src/argus/clients/yfinancy_client.py @@ -9,9 +9,6 @@ def get_timeseries(curr_symbol,start,end,interval): data = data.reset_index() data = data[["Date","Close"]] data = data.rename(columns={"Date": "date", "Close": "rate"}) - print(data.head()) - print(data.index) - print(data.columns) # Need to figure out, how to normalize the dataframe #result = data.reset_index(level=2,col_level=2,drop=True) #result = result[["Date", "Close"]] diff --git a/src/argus/gui/app.py b/src/argus/gui/app.py index c656154..cb70fca 100644 --- a/src/argus/gui/app.py +++ b/src/argus/gui/app.py @@ -77,9 +77,9 @@ def show_trend() -> None: global trend_canvas global trend_chart_widget - curr_symbol = "EURUSD=X", - start="2024-01-01", - end="2024-02-01", + curr_symbol = "EURUSD=X" + start="2024-01-01" + end="2025-01-01" interval="1d" calc_frame.pack_forget() @@ -92,13 +92,15 @@ def show_trend() -> None: if trend_canvas is None: fig = create_trendchart(curr_symbol,start,end,interval) + if fig is None: + return None fig.set_size_inches(7, 4) trend_canvas = FigureCanvasTkAgg(fig, master=content) trend_chart_widget = trend_canvas.get_tk_widget() if trend_chart_widget is None: - return + return None trend_canvas.draw() trend_chart_widget.pack(fill=tk.BOTH, expand=True) From 8b2887796e5142acf364ea269d684b1e74880755 Mon Sep 17 00:00:00 2001 From: Lev Gusiev Date: Sat, 20 Jun 2026 01:57:16 +0200 Subject: [PATCH 05/12] refactor(#33): mock client is legacy --- pyproject.toml | 1 + src/argus/clients/yfinancy_client.py | 4 - src/legacy/analytics/__init__.py | 0 src/legacy/analytics/charts/__init__.py | 0 src/legacy/analytics/charts/trend_chart.py | 72 ++++++++++ src/legacy/analytics/metrics/__init__.py | 0 src/legacy/analytics/metrics/trend_metrics.py | 75 +++++++++++ src/legacy/clients/__init__.py | 0 src/{argus => legacy}/clients/mock_client.py | 0 src/legacy/gui/app.py | 125 ++++++++++++++++++ src/legacy/services/timeseries_service.py | 32 +++++ tests/test_timeseries_service.py | 14 +- 12 files changed, 313 insertions(+), 10 deletions(-) create mode 100644 src/legacy/analytics/__init__.py create mode 100644 src/legacy/analytics/charts/__init__.py create mode 100644 src/legacy/analytics/charts/trend_chart.py create mode 100644 src/legacy/analytics/metrics/__init__.py create mode 100644 src/legacy/analytics/metrics/trend_metrics.py create mode 100644 src/legacy/clients/__init__.py rename src/{argus => legacy}/clients/mock_client.py (100%) create mode 100644 src/legacy/gui/app.py create mode 100644 src/legacy/services/timeseries_service.py diff --git a/pyproject.toml b/pyproject.toml index 665f963..e2dc784 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,7 @@ dependencies = [ "pandas", "numpy", "matplotlib", + "yfinance", ] [project.optional-dependencies] diff --git a/src/argus/clients/yfinancy_client.py b/src/argus/clients/yfinancy_client.py index 170289e..68b505e 100644 --- a/src/argus/clients/yfinancy_client.py +++ b/src/argus/clients/yfinancy_client.py @@ -9,9 +9,5 @@ def get_timeseries(curr_symbol,start,end,interval): data = data.reset_index() data = data[["Date","Close"]] data = data.rename(columns={"Date": "date", "Close": "rate"}) - # Need to figure out, how to normalize the dataframe - #result = data.reset_index(level=2,col_level=2,drop=True) - #result = result[["Date", "Close"]] - #result = result.rename(columns={"Date": "date", "Close": "rate"}) result = data.copy() return result \ No newline at end of file diff --git a/src/legacy/analytics/__init__.py b/src/legacy/analytics/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/legacy/analytics/charts/__init__.py b/src/legacy/analytics/charts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/legacy/analytics/charts/trend_chart.py b/src/legacy/analytics/charts/trend_chart.py new file mode 100644 index 0000000..e18aeb1 --- /dev/null +++ b/src/legacy/analytics/charts/trend_chart.py @@ -0,0 +1,72 @@ +import matplotlib.pyplot as plt +import pandas as pd +from legacy.services.timeseries_service import prepare_trend_analysis + + +def create_trendchart(curr: str, dates: pd.DataFrame): + """ + Create a trend chart for exchange-rate analysis. + + Builds a Matplotlib figure showing the exchange rate, its rolling + average, and the daily percentage change for a selected currency. + The minimum and maximum exchange-rate values are highlighted in the + chart. + + Args: + curr (str): Currency code or currency pair identifier used for + the trend analysis. + dates (pd.DataFrame): DataFrame containing the date information + used to prepare the time-series analysis. + + Returns: + matplotlib.figure.Figure: Matplotlib figure containing the trend + chart. + + Notes: + The chart uses two y-axes: + + - The left y-axis displays the exchange rate and rolling average. + - The right y-axis displays the daily percentage change. + + Minimum and maximum exchange-rate values are marked with scatter + points and annotations. + """ + df = pd.DataFrame() + df, min_max_rates = prepare_trend_analysis(curr, dates) + min_date = min_max_rates["min_date"][0] + min_rate = min_max_rates["min_rate"][0] + max_date = min_max_rates["max_date"][0] + max_rate = min_max_rates["max_rate"][0] + + # Rate and Rolling Average needs seperat x-Achse von Daily Percentage Chnage erhalten + fig, ax1 = plt.subplots(figsize=(5, 3.5), dpi=100) + + # Subplot 1 + ax1.plot(df["date"], df["rate"], color="black", label="Exchange Rate") + ax1.plot(df["date"], df["roll_avg"], color="blue", label="Rolling Average") + + # Scatter and Annote Min/Max Rate + ax1.scatter(min_date, min_rate, color="red") + ax1.scatter(max_date, max_rate, color="green") + ax1.annotate("Min", (min_date, min_rate)) + ax1.annotate("Max", (max_date, max_rate)) + + # Rotate date values for better visibillity + ax1.tick_params(axis="x", rotation=45) + + # Subplot 2 + ax2 = ax1.twinx() + bar_colors = ["green" if x >= 0 else "red" for x in df["daily_pct_change"]] + ax2.bar( + df["date"], + df["daily_pct_change"], + color=bar_colors, + alpha=0.4, + label="Daily Change", + ) + ax2.legend(loc="upper left") + ax2.set_ylabel("Percentage Scale") + + # Adjust the layout + fig.tight_layout() + return fig diff --git a/src/legacy/analytics/metrics/__init__.py b/src/legacy/analytics/metrics/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/legacy/analytics/metrics/trend_metrics.py b/src/legacy/analytics/metrics/trend_metrics.py new file mode 100644 index 0000000..5622acb --- /dev/null +++ b/src/legacy/analytics/metrics/trend_metrics.py @@ -0,0 +1,75 @@ +import pandas as pd + + +def add_daily_percentage_change(df: pd.DataFrame) -> pd.DataFrame: + """ + Add the daily percentage change of the exchange rate. + + Calculates the percentage change between each rate value and the + previous rate value. The result is added as a new column named + ``daily_pct_change``. + + Args: + df (pd.DataFrame): DataFrame containing at least a ``rate`` column. + + Returns: + pd.DataFrame: A copy of the input DataFrame with an added + ``daily_pct_change`` column. + + Notes: + The first row will contain ``NaN`` because there is no previous + rate value to compare against. + """ + result = df.copy() + result["daily_pct_change"] = result["rate"].pct_change() * 100 + return result + + +def add_rolling_average(df: pd.DataFrame) -> pd.DataFrame: + """ + Add a rolling average of the exchange rate. + + Calculates a rolling mean over the ``rate`` column using a fixed + window size of 3 rows. The result is added as a new column named + ``roll_avg``. + + Args: + df (pd.DataFrame): DataFrame containing at least a ``rate`` column. + + Returns: + pd.DataFrame: A copy of the input DataFrame with an added + ``roll_avg`` column. + """ + result = df.copy() + result["roll_avg"] = result["rate"].rolling(window=3, min_periods=1).mean() + return result + + +def get_min_max_rates(df: pd.DataFrame) -> dict: + """ + Get the minimum and maximum exchange-rate values. + + Finds the rows with the lowest and highest values in the ``rate`` + column and returns their dates and rates in a dictionary. + + Args: + df (pd.DataFrame): DataFrame containing at least ``date`` and + ``rate`` columns. + + Returns: + dict: Dictionary containing the minimum and maximum rate data with + the following keys: + + - ``min_date``: Date of the lowest exchange rate. + - ``min_rate``: Lowest exchange-rate value. + - ``max_date``: Date of the highest exchange rate. + - ``max_rate``: Highest exchange-rate value. + """ + min_max = {"min_date": [], "min_rate": [], "max_date": [], "max_rate": []} + min_id = df["rate"].idxmin() + max_id = df["rate"].idxmax() + min_max["min_date"].append(df.loc[min_id, "date"]) + min_max["min_rate"].append(df.loc[min_id, "rate"]) + min_max["max_date"].append(df.loc[max_id, "date"]) + min_max["max_rate"].append(df.loc[max_id, "rate"]) + return min_max diff --git a/src/legacy/clients/__init__.py b/src/legacy/clients/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/argus/clients/mock_client.py b/src/legacy/clients/mock_client.py similarity index 100% rename from src/argus/clients/mock_client.py rename to src/legacy/clients/mock_client.py diff --git a/src/legacy/gui/app.py b/src/legacy/gui/app.py new file mode 100644 index 0000000..cf52257 --- /dev/null +++ b/src/legacy/gui/app.py @@ -0,0 +1,125 @@ +import tkinter as tk +import pandas as pd +from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg +from legacy.analytics.charts.trend_chart import create_trendchart + + +def on_close() -> None: + """ + Handles the closing of the application window. It ensures that any open trend chart is destroyed and + the application is properly closed. + """ + if trend_chart_widget is not None: + trend_chart_widget.destroy() + + root.quit() + root.destroy() + + +def hide_trend_chart() -> None: + """ + Hides the trend chart from the GUI if it is currently displayed. + """ + if trend_chart_widget is not None: + trend_chart_widget.pack_forget() + + +def show_menu() -> None: + """ + Displays the main menu in the application. It updates the GUI to show the menu interface. + """ + app_frame.pack_forget() + hide_trend_chart() + + menu_frame.pack(side="right", fill=tk.BOTH, expand=True) + +def show_trend() -> None: + """ + Displays the trend chart in the application. It prepares the data for trend analysis, + creates the trend chart, and updates the GUI to show the chart. + """ + global trend_canvas + global trend_chart_widget + + mock_dates = { + "date": [ + "2026-06-01", + "2026-06-02", + "2026-06-03", + "2026-06-04", + "2026-06-05", + "2026-06-06", + "2026-06-07", + "2026-06-08", + "2026-06-09", + "2026-06-10", + "2026-06-11", + "2026-06-12", + "2026-06-13", + "2026-06-14", + "2026-06-15", + ] + } + mock_dates = pd.DataFrame(mock_dates) + mock_curr = "USD" + + menu_frame.pack_forget() + + app_frame.pack(fill=tk.BOTH, expand=True) + sidebar.pack(side="top", fill="x") + content.pack(side="top", fill=tk.BOTH, expand=True) + + if trend_canvas is None: + fig = create_trendchart(mock_curr, mock_dates) + fig.set_size_inches(7, 4) + + trend_canvas = FigureCanvasTkAgg(fig, master=content) + trend_chart_widget = trend_canvas.get_tk_widget() + + if trend_chart_widget is None: + return + trend_canvas.draw() + trend_chart_widget.pack(fill=tk.BOTH, expand=True) + + +def app() -> None: + """ + The main function that initializes and starts the GUI application. It sets up the main window, frames, labels, entries, and buttons, and defines the layout of the application. + """ + root.mainloop() + + +# Window +root = tk.Tk() +root.title("FX-Converter Lab") +root.geometry("800x600") # Width x Length +root.protocol("WM_DELETE_WINDOW", on_close) + +# Frames +menu_frame = tk.Frame(root) +app_frame = tk.Frame(root) +sidebar = tk.Frame(app_frame) +content = tk.Frame(app_frame) + +# Trend chart is loaded lazily +trend_canvas = None +trend_chart_widget = None + +# Labels +menu_label = tk.Label(menu_frame, text="Menu", font=("Arial", 20)) +menu_label.pack(pady=20) + + +# Buttons +from_menu_trend_chart = tk.Button(menu_frame, text="Trend Chart", command=show_trend) +from_sidebar_trend_chart = tk.Button(sidebar, text="Trend Chart", command=show_trend) +return_menu = tk.Button(sidebar, text="Back to menu", command=show_menu) + +from_menu_trend_chart.pack(fill="x", padx=50, pady=15) +from_sidebar_trend_chart.pack(side="left") +return_menu.pack(side="left") + +show_menu() + +if __name__ == "__main__": + app() diff --git a/src/legacy/services/timeseries_service.py b/src/legacy/services/timeseries_service.py new file mode 100644 index 0000000..d79d599 --- /dev/null +++ b/src/legacy/services/timeseries_service.py @@ -0,0 +1,32 @@ +import pandas as pd +from legacy.clients.mock_client import get_mock_timeseries +from legacy.analytics.metrics.trend_metrics import ( + add_rolling_average, + add_daily_percentage_change, + get_min_max_rates, +) + + +def prepare_trend_analysis( + mock_curr: str, df: pd.DataFrame +) -> tuple[pd.DataFrame, dict]: + """ + Prepares the data for trend analysis by adding conversion rates, daily percentage change, and rolling average. + + Arg1: mock_curr: str - the currency code for which the trend analysis is to + be performed + Arg2: df: pd.DataFrame - the DataFrame containing the dates for which the + conversion rates are to be added + + Return: tuple[pd.DataFrame, dict] - a tuple containing the updated DataFrame with conversion rates, + daily percentage change, and rolling average, and a dictionary with the minimum and maximum rates + """ + df["rate"] = 0.0 + # For each date one API call to get the rate + for i in range(len(df)): + date = str(df.loc[i, "date"]) + df.loc[i, "rate"] = get_mock_timeseries(mock_curr, date) + df = add_daily_percentage_change(df) + df = add_rolling_average(df) + min_max_rates = get_min_max_rates(df) + return df, min_max_rates diff --git a/tests/test_timeseries_service.py b/tests/test_timeseries_service.py index dbc5675..83b6e1c 100644 --- a/tests/test_timeseries_service.py +++ b/tests/test_timeseries_service.py @@ -5,11 +5,10 @@ def test_is_pct_change_added(): - test_curr = "USD" - test_dates = { - "date": ["2026-06-01", "2026-06-02", "2026-06-03"], - } - test_dates = pd.DataFrame(test_dates) + test_curr = "EURUSD=X" + test_start = "" + test_end = "" + test_interval = "1d" expect_result = { "date": ["2026-06-01", "2026-06-02", "2026-06-03"], @@ -23,7 +22,10 @@ def test_is_pct_change_added(): "max_date": ["2026-06-03"], "max_rate": [1.14], } - result_df, result_dict = prepare_trend_analysis(test_curr, test_dates) + result = prepare_trend_analysis(test_curr, test_start,test_end,test_interval) + if result is None: + return None + result_df, result_dict = result expect_df = pd.DataFrame(expect_result) pdt.assert_frame_equal(result_df, expect_df) From 361ba1c3bbd84a389419f6d8d1191e68f9f535b8 Mon Sep 17 00:00:00 2001 From: Lev Gusiev Date: Sat, 20 Jun 2026 02:21:12 +0200 Subject: [PATCH 06/12] test(#33): add a test service --- tests/test_timeseries_service.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/tests/test_timeseries_service.py b/tests/test_timeseries_service.py index 83b6e1c..4b7ac40 100644 --- a/tests/test_timeseries_service.py +++ b/tests/test_timeseries_service.py @@ -4,28 +4,31 @@ from argus.services.timeseries_service import prepare_trend_analysis -def test_is_pct_change_added(): +def test_get_a_full_timeseries(): test_curr = "EURUSD=X" - test_start = "" - test_end = "" + test_start = "2024-01-01" + test_end = "2024-01-04" test_interval = "1d" expect_result = { - "date": ["2026-06-01", "2026-06-02", "2026-06-03"], - "rate": [1.08, 1.1, 1.14], - "daily_pct_change": [np.nan, 1.85185185185186, 3.6363636363636154], - "roll_avg": [1.08, 1.09, 1.1066666666666667], + "date": ["2024-01-01", "2024-01-02", "2024-01-03"], + "rate": [1.1055831909179688, 1.1038745641708374, 1.0941756963729858], + "daily_pct_change": [np.nan, -0.1545452898675692, -0.8786204622023064], + "roll_avg": [1.1055831909179688, 1.104728877544403, 1.101211150487264], } expect_dict = { - "min_date": ["2026-06-01"], - "min_rate": [1.08], - "max_date": ["2026-06-03"], - "max_rate": [1.14], + "min_date": ["2024-01-03 00:00:00"], + "min_rate": [1.0941756963729858], + "max_date": ["2024-01-01 00:00:00"], + "max_rate": [1.1055831909179688], } result = prepare_trend_analysis(test_curr, test_start,test_end,test_interval) if result is None: - return None + return False result_df, result_dict = result + result_df["date"] = result_df["date"].astype("str") + result_dict["min_date"] = [str(result_dict["min_date"][0])] + result_dict["max_date"] = [str(result_dict["max_date"][0])] expect_df = pd.DataFrame(expect_result) pdt.assert_frame_equal(result_df, expect_df) From 4ffecf90ee8beae46a5e892d682ad7d9e0151e22 Mon Sep 17 00:00:00 2001 From: Lev Gusiev Date: Sat, 20 Jun 2026 02:23:20 +0200 Subject: [PATCH 07/12] style(#33): ruff reformat --- src/argus/analytics/charts/trend_chart.py | 4 ++-- src/argus/clients/yfinancy_client.py | 18 ++++++++++++------ src/argus/gui/app.py | 8 ++++---- src/argus/services/timeseries_service.py | 8 +++++--- src/legacy/gui/app.py | 1 + tests/test_timeseries_service.py | 2 +- 6 files changed, 25 insertions(+), 16 deletions(-) diff --git a/src/argus/analytics/charts/trend_chart.py b/src/argus/analytics/charts/trend_chart.py index f889b83..462cffc 100644 --- a/src/argus/analytics/charts/trend_chart.py +++ b/src/argus/analytics/charts/trend_chart.py @@ -3,7 +3,7 @@ from argus.services.timeseries_service import prepare_trend_analysis -def create_trendchart(curr_symbol: str, start:str,end:str,interval:str): +def create_trendchart(curr_symbol: str, start: str, end: str, interval: str): """ Create a trend chart for exchange-rate analysis. @@ -31,7 +31,7 @@ def create_trendchart(curr_symbol: str, start:str,end:str,interval:str): Minimum and maximum exchange-rate values are marked with scatter points and annotations. """ - result = prepare_trend_analysis(curr_symbol,start,end,interval) + result = prepare_trend_analysis(curr_symbol, start, end, interval) if result is None: return None df, min_max_rates = result diff --git a/src/argus/clients/yfinancy_client.py b/src/argus/clients/yfinancy_client.py index 68b505e..9d903ed 100644 --- a/src/argus/clients/yfinancy_client.py +++ b/src/argus/clients/yfinancy_client.py @@ -1,13 +1,19 @@ import yfinance as yf -def get_timeseries(curr_symbol,start,end,interval): - data = yf.download(tickers=curr_symbol, - start=start,end=end,interval=interval,multi_level_index=False,progress=False - ) + +def get_timeseries(curr_symbol, start, end, interval): + data = yf.download( + tickers=curr_symbol, + start=start, + end=end, + interval=interval, + multi_level_index=False, + progress=False, + ) if data is None: return None data = data.reset_index() - data = data[["Date","Close"]] + data = data[["Date", "Close"]] data = data.rename(columns={"Date": "date", "Close": "rate"}) result = data.copy() - return result \ No newline at end of file + return result diff --git a/src/argus/gui/app.py b/src/argus/gui/app.py index cb70fca..3801998 100644 --- a/src/argus/gui/app.py +++ b/src/argus/gui/app.py @@ -78,9 +78,9 @@ def show_trend() -> None: global trend_chart_widget curr_symbol = "EURUSD=X" - start="2024-01-01" - end="2025-01-01" - interval="1d" + start = "2024-01-01" + end = "2025-01-01" + interval = "1d" calc_frame.pack_forget() conv_frame.pack_forget() @@ -91,7 +91,7 @@ def show_trend() -> None: content.pack(side="top", fill=tk.BOTH, expand=True) if trend_canvas is None: - fig = create_trendchart(curr_symbol,start,end,interval) + fig = create_trendchart(curr_symbol, start, end, interval) if fig is None: return None fig.set_size_inches(7, 4) diff --git a/src/argus/services/timeseries_service.py b/src/argus/services/timeseries_service.py index b506162..82f1a97 100644 --- a/src/argus/services/timeseries_service.py +++ b/src/argus/services/timeseries_service.py @@ -7,7 +7,9 @@ ) -def prepare_trend_analysis(curr_symbol: str, start:str,end:str,intervall:str) -> tuple[pd.DataFrame, dict] | None: +def prepare_trend_analysis( + curr_symbol: str, start: str, end: str, intervall: str +) -> tuple[pd.DataFrame, dict] | None: """ Prepares the data for trend analysis by adding conversion rates, daily percentage change, and rolling average. @@ -19,8 +21,8 @@ def prepare_trend_analysis(curr_symbol: str, start:str,end:str,intervall:str) -> Return: tuple[pd.DataFrame, dict] - a tuple containing the updated DataFrame with conversion rates, daily percentage change, and rolling average, and a dictionary with the minimum and maximum rates """ - - df = get_timeseries(curr_symbol, start,end,intervall) + + df = get_timeseries(curr_symbol, start, end, intervall) if df is None: return None df = add_daily_percentage_change(df) diff --git a/src/legacy/gui/app.py b/src/legacy/gui/app.py index cf52257..ff9d790 100644 --- a/src/legacy/gui/app.py +++ b/src/legacy/gui/app.py @@ -33,6 +33,7 @@ def show_menu() -> None: menu_frame.pack(side="right", fill=tk.BOTH, expand=True) + def show_trend() -> None: """ Displays the trend chart in the application. It prepares the data for trend analysis, diff --git a/tests/test_timeseries_service.py b/tests/test_timeseries_service.py index 4b7ac40..7dd3c9f 100644 --- a/tests/test_timeseries_service.py +++ b/tests/test_timeseries_service.py @@ -22,7 +22,7 @@ def test_get_a_full_timeseries(): "max_date": ["2024-01-01 00:00:00"], "max_rate": [1.1055831909179688], } - result = prepare_trend_analysis(test_curr, test_start,test_end,test_interval) + result = prepare_trend_analysis(test_curr, test_start, test_end, test_interval) if result is None: return False result_df, result_dict = result From 4b6c73e34c69ed73bc85b481e8229391093bbe7f Mon Sep 17 00:00:00 2001 From: Lev Gusiev Date: Sat, 20 Jun 2026 02:26:28 +0200 Subject: [PATCH 08/12] style(#33): unused lib remove --- src/argus/analytics/charts/trend_chart.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/argus/analytics/charts/trend_chart.py b/src/argus/analytics/charts/trend_chart.py index 462cffc..1293361 100644 --- a/src/argus/analytics/charts/trend_chart.py +++ b/src/argus/analytics/charts/trend_chart.py @@ -1,5 +1,4 @@ import matplotlib.pyplot as plt -import pandas as pd from argus.services.timeseries_service import prepare_trend_analysis From 262661a21c8a4b561bee7adebe5a9f9de2a0b3c6 Mon Sep 17 00:00:00 2001 From: Lev Gusiev Date: Sat, 20 Jun 2026 02:28:17 +0200 Subject: [PATCH 09/12] style(#33): remove unused lib --- src/argus/gui/app.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/argus/gui/app.py b/src/argus/gui/app.py index 3801998..9a19523 100644 --- a/src/argus/gui/app.py +++ b/src/argus/gui/app.py @@ -1,5 +1,4 @@ import tkinter as tk -import pandas as pd from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg from argus.analytics.charts.trend_chart import create_trendchart from argus.services.calculator_service import calc, check_op From 3cef9a80e8ac8f46be4e3328f9270ff6990b0ee9 Mon Sep 17 00:00:00 2001 From: Lev Gusiev Date: Sat, 20 Jun 2026 11:56:24 +0200 Subject: [PATCH 10/12] test(#33): add tests for client --- src/argus/clients/yfinance_client.py | 30 ++++++++++ src/argus/clients/yfinancy_client.py | 19 ------- src/argus/services/timeseries_service.py | 2 +- tests/test_yfinance_client.py | 72 ++++++++++++++++++++++++ 4 files changed, 103 insertions(+), 20 deletions(-) create mode 100644 src/argus/clients/yfinance_client.py delete mode 100644 src/argus/clients/yfinancy_client.py create mode 100644 tests/test_yfinance_client.py diff --git a/src/argus/clients/yfinance_client.py b/src/argus/clients/yfinance_client.py new file mode 100644 index 0000000..f25b66b --- /dev/null +++ b/src/argus/clients/yfinance_client.py @@ -0,0 +1,30 @@ +import yfinance as yf +import logging + + +def get_timeseries(curr_symbol, start, end, interval): + try: + yf_logger = logging.getLogger("yfinance") + yf_logger.disabled = True + data = yf.download( + tickers=curr_symbol, + start=start, + end=end, + interval=interval, + multi_level_index=False, + progress=False, + ) + yf_logger.disabled = False + if data is None: + return None + if data.empty: + return None + data = data.reset_index() + data = data[["Date", "Close"]] + data = data.rename(columns={"Date": "date", "Close": "rate"}) + return data + except Exception: + return None + + +get_timeseries("EURUSD=X","2024-01-01","2024-01-02","1m") \ No newline at end of file diff --git a/src/argus/clients/yfinancy_client.py b/src/argus/clients/yfinancy_client.py deleted file mode 100644 index 9d903ed..0000000 --- a/src/argus/clients/yfinancy_client.py +++ /dev/null @@ -1,19 +0,0 @@ -import yfinance as yf - - -def get_timeseries(curr_symbol, start, end, interval): - data = yf.download( - tickers=curr_symbol, - start=start, - end=end, - interval=interval, - multi_level_index=False, - progress=False, - ) - if data is None: - return None - data = data.reset_index() - data = data[["Date", "Close"]] - data = data.rename(columns={"Date": "date", "Close": "rate"}) - result = data.copy() - return result diff --git a/src/argus/services/timeseries_service.py b/src/argus/services/timeseries_service.py index 82f1a97..9fe7796 100644 --- a/src/argus/services/timeseries_service.py +++ b/src/argus/services/timeseries_service.py @@ -1,5 +1,5 @@ import pandas as pd -from argus.clients.yfinancy_client import get_timeseries +from argus.clients.yfinance_client import get_timeseries from argus.analytics.metrics.trend_metrics import ( add_rolling_average, add_daily_percentage_change, diff --git a/tests/test_yfinance_client.py b/tests/test_yfinance_client.py new file mode 100644 index 0000000..7e46c48 --- /dev/null +++ b/tests/test_yfinance_client.py @@ -0,0 +1,72 @@ +from argus.clients.yfinance_client import get_timeseries +import pandas as pd +import pandas.testing as pdt + +def test_get_dataframe(monkeypatch): + test_resp = pd.DataFrame( + { + "Close": [1.105583, 1.103875, 1.094176], + }, + index=pd.to_datetime(["2024-01-01", "2024-01-02", "2024-01-03"]), + ) + test_resp.index.name = "Date" + test_curr = "EURUSD=X" + test_start = "2024-01-01" + test_end = "2024-01-04" + test_interval = "1d" + + + def fake_yfinance_download(*args, **kwargs): + return test_resp + + monkeypatch.setattr("yfinance.download", fake_yfinance_download) + + result = get_timeseries(test_curr,test_start,test_end,test_interval) + expected = pd.DataFrame({ + "date": pd.to_datetime(["2024-01-01", "2024-01-02", "2024-01-03"]), + "rate": [1.105583, 1.103875, 1.094176]}) + assert result is not None + pdt.assert_frame_equal(result, expected) + +def test_get_none(monkeypatch): + test_curr = "EURUSD=X" + test_start = "2024-01-01" + test_end = "2024-01-04" + test_interval = "1d" + + def fake_yfinance_download(*args, **kwargs): + return None + + monkeypatch.setattr("yfinance.download", fake_yfinance_download) + + result = get_timeseries(test_curr,test_start,test_end,test_interval) + assert result is None + +def test_get_empty_frame(monkeypatch): + test_curr = "EURUSD=X" + test_start = "2024-01-01" + test_end = "2024-01-01" + test_interval = "1d" + + def fake_yfinance_download(*args, **kwargs): + return pd.DataFrame() + + monkeypatch.setattr("yfinance.download", fake_yfinance_download) + + result = get_timeseries(test_curr,test_start,test_end,test_interval) + assert result is None + +def test_error_raise(monkeypatch): + test_curr = "EURUSD=X" + # start date is inclusiv and end date is exclusiv - the range 2024-01-01-2024-01-01 is not possible + test_start = "2024-01-04" + test_end = "2024-01-02" + test_interval = "1d" + + def fake_yfinance_download(tickers=test_curr,start=test_start,end=test_end,interval=test_interval): + return Exception("fake yfinance error") + + monkeypatch.setattr("yfinance.download", fake_yfinance_download) + + result = get_timeseries(test_curr,test_start,test_end,test_interval) + assert result is None \ No newline at end of file From a7ddfe1e5a66782e802bf86bc617c75a9cc1f264 Mon Sep 17 00:00:00 2001 From: Lev Gusiev Date: Sat, 20 Jun 2026 12:00:59 +0200 Subject: [PATCH 11/12] style(#33): reformat with ruff --- src/argus/clients/yfinance_client.py | 4 +-- tests/test_yfinance_client.py | 46 ++++++++++++++++------------ 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/src/argus/clients/yfinance_client.py b/src/argus/clients/yfinance_client.py index f25b66b..88f1527 100644 --- a/src/argus/clients/yfinance_client.py +++ b/src/argus/clients/yfinance_client.py @@ -26,5 +26,5 @@ def get_timeseries(curr_symbol, start, end, interval): except Exception: return None - -get_timeseries("EURUSD=X","2024-01-01","2024-01-02","1m") \ No newline at end of file + +get_timeseries("EURUSD=X", "2024-01-01", "2024-01-02", "1m") diff --git a/tests/test_yfinance_client.py b/tests/test_yfinance_client.py index 7e46c48..faf15fc 100644 --- a/tests/test_yfinance_client.py +++ b/tests/test_yfinance_client.py @@ -2,10 +2,11 @@ import pandas as pd import pandas.testing as pdt + def test_get_dataframe(monkeypatch): test_resp = pd.DataFrame( { - "Close": [1.105583, 1.103875, 1.094176], + "Close": [1.105583, 1.103875, 1.094176], }, index=pd.to_datetime(["2024-01-01", "2024-01-02", "2024-01-03"]), ) @@ -15,19 +16,22 @@ def test_get_dataframe(monkeypatch): test_end = "2024-01-04" test_interval = "1d" - def fake_yfinance_download(*args, **kwargs): return test_resp - + monkeypatch.setattr("yfinance.download", fake_yfinance_download) - - result = get_timeseries(test_curr,test_start,test_end,test_interval) - expected = pd.DataFrame({ - "date": pd.to_datetime(["2024-01-01", "2024-01-02", "2024-01-03"]), - "rate": [1.105583, 1.103875, 1.094176]}) + + result = get_timeseries(test_curr, test_start, test_end, test_interval) + expected = pd.DataFrame( + { + "date": pd.to_datetime(["2024-01-01", "2024-01-02", "2024-01-03"]), + "rate": [1.105583, 1.103875, 1.094176], + } + ) assert result is not None pdt.assert_frame_equal(result, expected) + def test_get_none(monkeypatch): test_curr = "EURUSD=X" test_start = "2024-01-01" @@ -36,12 +40,13 @@ def test_get_none(monkeypatch): def fake_yfinance_download(*args, **kwargs): return None - + monkeypatch.setattr("yfinance.download", fake_yfinance_download) - - result = get_timeseries(test_curr,test_start,test_end,test_interval) + + result = get_timeseries(test_curr, test_start, test_end, test_interval) assert result is None + def test_get_empty_frame(monkeypatch): test_curr = "EURUSD=X" test_start = "2024-01-01" @@ -50,12 +55,13 @@ def test_get_empty_frame(monkeypatch): def fake_yfinance_download(*args, **kwargs): return pd.DataFrame() - + monkeypatch.setattr("yfinance.download", fake_yfinance_download) - - result = get_timeseries(test_curr,test_start,test_end,test_interval) + + result = get_timeseries(test_curr, test_start, test_end, test_interval) assert result is None + def test_error_raise(monkeypatch): test_curr = "EURUSD=X" # start date is inclusiv and end date is exclusiv - the range 2024-01-01-2024-01-01 is not possible @@ -63,10 +69,12 @@ def test_error_raise(monkeypatch): test_end = "2024-01-02" test_interval = "1d" - def fake_yfinance_download(tickers=test_curr,start=test_start,end=test_end,interval=test_interval): + def fake_yfinance_download( + tickers=test_curr, start=test_start, end=test_end, interval=test_interval + ): return Exception("fake yfinance error") - + monkeypatch.setattr("yfinance.download", fake_yfinance_download) - - result = get_timeseries(test_curr,test_start,test_end,test_interval) - assert result is None \ No newline at end of file + + result = get_timeseries(test_curr, test_start, test_end, test_interval) + assert result is None From d9c62812f278164282346138e5295a6ce3d08781 Mon Sep 17 00:00:00 2001 From: Lev Gusiev Date: Sat, 20 Jun 2026 12:03:20 +0200 Subject: [PATCH 12/12] docs(#33): add docstrings --- src/argus/clients/yfinance_client.py | 19 ++++++++++++++++--- src/argus/services/timeseries_service.py | 24 +++++++++++++++++------- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/src/argus/clients/yfinance_client.py b/src/argus/clients/yfinance_client.py index 88f1527..de86041 100644 --- a/src/argus/clients/yfinance_client.py +++ b/src/argus/clients/yfinance_client.py @@ -3,6 +3,22 @@ def get_timeseries(curr_symbol, start, end, interval): + """ + Fetch historical exchange-rate time series data from Yahoo Finance. + + Args: + curr_symbol (str): Currency symbol used by Yahoo Finance, for example + "EURUSD=X". + start (str): Start date of the requested time range in YYYY-MM-DD format. + end (str): End date of the requested time range in YYYY-MM-DD format. + interval (str): Data interval supported by Yahoo Finance, for example + "1d", "1h", or "15m". + + Returns: + pandas.DataFrame | None: A DataFrame containing the columns ``date`` and + ``rate`` if data was successfully fetched. Returns ``None`` if the + request fails, returns no data, or an exception occurs. + """ try: yf_logger = logging.getLogger("yfinance") yf_logger.disabled = True @@ -25,6 +41,3 @@ def get_timeseries(curr_symbol, start, end, interval): return data except Exception: return None - - -get_timeseries("EURUSD=X", "2024-01-01", "2024-01-02", "1m") diff --git a/src/argus/services/timeseries_service.py b/src/argus/services/timeseries_service.py index 9fe7796..b6251bb 100644 --- a/src/argus/services/timeseries_service.py +++ b/src/argus/services/timeseries_service.py @@ -11,15 +11,25 @@ def prepare_trend_analysis( curr_symbol: str, start: str, end: str, intervall: str ) -> tuple[pd.DataFrame, dict] | None: """ - Prepares the data for trend analysis by adding conversion rates, daily percentage change, and rolling average. + Prepare time-series data for trend analysis. - Arg1: mock_curr: str - the currency code for which the trend analysis is to - be performed - Arg2: df: pd.DataFrame - the DataFrame containing the dates for which the - conversion rates are to be added + Fetches historical exchange-rate data for the given currency symbol and + enriches it with daily percentage changes and a rolling average. It also + calculates the minimum and maximum exchange rates for the resulting time + series. - Return: tuple[pd.DataFrame, dict] - a tuple containing the updated DataFrame with conversion rates, - daily percentage change, and rolling average, and a dictionary with the minimum and maximum rates + Args: + curr_symbol (str): Currency symbol used by Yahoo Finance, for example + "EURUSD=X". + start (str): Start date of the requested time range in YYYY-MM-DD format. + end (str): End date of the requested time range in YYYY-MM-DD format. + intervall (str): Data interval supported by Yahoo Finance, for example + "1d", "1h", or "15m". + + Returns: + tuple[pd.DataFrame, dict] | None: A tuple containing the prepared + DataFrame and a dictionary with minimum and maximum rates. Returns + ``None`` if no time-series data could be fetched. """ df = get_timeseries(curr_symbol, start, end, intervall)