Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
419 changes: 419 additions & 0 deletions docs/research-data-sources

Large diffs are not rendered by default.

27 changes: 27 additions & 0 deletions src/argus/analytics/charts/trend_chart.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,33 @@


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]
Expand Down
51 changes: 51 additions & 0 deletions src/argus/analytics/metrics/trend_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,69 @@


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()
Expand Down
34 changes: 22 additions & 12 deletions src/argus/clients/exchangerate_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,16 @@
)


def get_rates(curr1, curr2):
def get_rates(curr1: str, curr2: str):
"""
Get the exchange rate between two currencies using the ExchangeRate-API.

Args:
curr1 (str): The base currency code (e.g., "USD").
curr2 (str): The target currency code (e.g., "EUR").

Returns: A dictionary containing the result status, error type (if any), and conversion rate (if successful).
"""
url = f"{EXCHANGE_RATE_BASE_URL}/{EXCHANGE_RATE_API_KEY}/pair/{curr1}/{curr2}"
data = {"result": "", "error_type": "", "conversion_rate": None}

Expand Down Expand Up @@ -43,23 +52,24 @@ def get_rates(curr1, curr2):
return None


def check_error(err_type):
def check_error(err_type: str) -> None:
"""
Check the error type returned by the API and print an appropriate message.

Args: err_type (str): The error type returned by the API.

Returns: None
"""
match err_type:
case "unsupported-code" | "malformed-request":
print("Ungültige Anfrage! Bitter versuchen Sie es später erneut.")
print("Invalid request! Please try again later.")
case "invalid-key":
print(
"Ungültiger API-Key! Checken Sie Ihren API-Key und versuchen Sie es erneut."
)
print("Invalid API key! Please check your API key and try again.")
case "inactive-account":
print(
"Inaktives Konto! Bitte auf exchangerate-api.com gehen und Konto aktivieren."
"Inactive account! Please go to exchangerate-api.com and activate your account."
)
case "quota-reached":
print(
"Anfrage-Limit erreicht! Bitte später erneut versuchen oder auf exchangerate-api.com upgraden."
"Request limit reached! Please try again later or upgrade to exchangerate-api.com."
)


# Testen, ob die API funktioniert
# data = get_rates("EUR", "USD")
28 changes: 28 additions & 0 deletions src/argus/domain/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,19 +167,47 @@


def normalize_input_string(input: str) -> str:
"""
Normalizes the input string by stripping leading and trailing whitespace and converting it to uppercase.

Arg1: input: str - the input string to be normalized

Return: str - the normalized input string
"""
return input.strip().upper()


def parse_amount(value: str) -> float | None:
"""
Parses the input string to a float. If the input is not a valid number, it returns None.

Arg1: value: str - the input string to be parsed as a float

Return: float or None - the parsed float value if valid, otherwise None
"""
try:
return float(value)
except ValueError:
return None


def is_valid_curr_code(code: str) -> bool:
"""
Checks if the given currency code is valid.

Arg1: code: str - the currency code to be checked

Return: bool - True if the currency code is valid, otherwise False
"""
return code in VALID_CURRENCY_CODES


def is_valid_op(op: str) -> bool:
"""
Checks if the given operation is valid.

Arg1: op: str - the operation to be checked

Return: bool - True if the operation is valid, otherwise False
"""
return op in VALID_OPS
33 changes: 33 additions & 0 deletions src/argus/gui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@


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()

Expand All @@ -16,11 +20,17 @@ def on_close() -> None:


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()
calc_frame.pack_forget()
conv_frame.pack_forget()
Expand All @@ -30,6 +40,9 @@ def show_menu() -> None:


def show_calc() -> None:
"""
Displays the calculator in the application. It updates the GUI to show the calculator interface.
"""
conv_frame.pack_forget()
hide_trend_chart()
menu_frame.pack_forget()
Expand All @@ -42,6 +55,9 @@ def show_calc() -> None:


def show_conv() -> None:
"""
Displays the currency converter in the application. It updates the GUI to show the converter interface.
"""
calc_frame.pack_forget()
hide_trend_chart()
menu_frame.pack_forget()
Expand All @@ -54,6 +70,10 @@ def show_conv() -> None:


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

Expand Down Expand Up @@ -101,6 +121,11 @@ def show_trend() -> None:


def act_calculate() -> None:
"""
Handles the calculation action when the "Calculate" button is clicked.
It checks the validity of the input numbers and operator, performs the calculation,
and updates the result label accordingly.
"""
resp1 = num1.get()
resp1 = parse_amount(resp1)

Expand Down Expand Up @@ -131,6 +156,11 @@ def act_calculate() -> None:


def act_convert() -> None:
"""
Handles the conversion action when the "Convert" button is clicked.
It checks the validity of the input currencies and amount, performs the conversion,
and updates the result label accordingly.
"""
resp1 = check_currency(curr1.get())
resp2 = check_currency(curr2.get())
amount = parse_amount(amount_e.get())
Expand Down Expand Up @@ -162,6 +192,9 @@ def act_convert() -> None:


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()


Expand Down
3 changes: 3 additions & 0 deletions src/argus/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@


def main() -> None:
"""
The main function that starts the application.
"""
app()


Expand Down
18 changes: 16 additions & 2 deletions src/argus/services/calculator_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,29 @@


def check_op(op: str) -> bool:
# Tipp: Liste verwenden, wenn mehr als 2 Optionen für etwas besteht
"""
Checks if the input operator is valid.

Arg1: op: str - the operator to be checked for validity

Return: bool - True if the operator is valid, otherwise False
"""
if is_valid_op(op):
return True
else:
return False


def calc(num1: float, num2: float, op: str) -> float | None:
# Tipp: Auf Ifelse verzichten, wenn davon mehr als 3 Stück entstehen
"""
Performs a calculation based on the provided operator.

Arg1: num1: float - the first number
Arg2: num2: float - the second number
Arg3: op: str - the operator to be used for the calculation

Return: float or None - the result of the calculation if valid, otherwise None
"""
match op:
case "+":
return num1 + num2
Expand Down
27 changes: 26 additions & 1 deletion src/argus/services/conversion_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,14 @@
from argus.domain.validation import normalize_input_string, is_valid_curr_code


# This function has to be moved to dmoain
def check_currency(question: str) -> str | None:
"""
Checks if the input question contains a valid currency code.

Arg1: question: str - the question to be checked for a valid currency code

Return: str or None - the valid currency code if found, otherwise None
"""
resp = normalize_input_string(question)

if is_valid_curr_code(resp):
Expand All @@ -13,6 +19,15 @@ def check_currency(question: str) -> str | None:


def get_conv_rate(resp1: str, resp2: str) -> float | None:
"""
Gets the conversion rate between two currencies.

Arg1: resp1: str - the first currency code
Arg2: resp2: str - the second currency code

Return: float or None - the conversion rate if found, otherwise None
"""

data = ex_client.get_rates(resp1, resp2)

if data is None:
Expand All @@ -22,6 +37,16 @@ def get_conv_rate(resp1: str, resp2: str) -> float | None:


def convert(amount: float, resp1: str, resp2: str) -> float | None:
"""
Converts an amount from one currency to another using the conversion rate.

Arg1: amount: float - the amount to be converted
Arg2: resp1: str - the first currency code
Arg3: resp2: str - the second currency code

Return: float or None - the converted amount if conversion rate is found, otherwise None
"""

data = get_conv_rate(resp1, resp2)
if data is not None:
return amount * data
Expand Down
Loading
Loading