diff --git a/docs/research-data-sources b/docs/research-data-sources.md similarity index 100% rename from docs/research-data-sources rename to docs/research-data-sources.md diff --git a/src/argus/clients/exchangerate_client.py b/src/argus/clients/exchangerate_client.py index 8ea5e89..1aa189e 100644 --- a/src/argus/clients/exchangerate_client.py +++ b/src/argus/clients/exchangerate_client.py @@ -1,12 +1,13 @@ -import requests as req +import requests as reqs from argus.config import ( EXCHANGE_RATE_BASE_URL, EXCHANGE_RATE_API_KEY, REQUEST_TIMEOUT_SECONDS, ) +from argus.domain.internal_models import MarketDataRequest -def get_rates(curr1: str, curr2: str): +def get_rates(req: MarketDataRequest) -> dict | None: """ Get the exchange rate between two currencies using the ExchangeRate-API. @@ -16,21 +17,23 @@ def get_rates(curr1: str, curr2: str): Returns: A dictionary containing the result status, error type (if any), and conversion rate (if successful). """ + curr1 = req.instrument.base_currency + curr2 = req.instrument.quote_currency url = f"{EXCHANGE_RATE_BASE_URL}/{EXCHANGE_RATE_API_KEY}/pair/{curr1}/{curr2}" data = {"result": "", "error_type": "", "conversion_rate": None} try: - resp = req.get(url, timeout=REQUEST_TIMEOUT_SECONDS) + resp = reqs.get(url, timeout=REQUEST_TIMEOUT_SECONDS) resp.raise_for_status() payload = resp.json() - except req.exceptions.Timeout: + except reqs.exceptions.Timeout: print("API hat zu lange gebraucht.") return None - except req.exceptions.ConnectionError: + except reqs.exceptions.ConnectionError: print("Keine Verbindung zur API.") return None - except req.exceptions.RequestException as error: + except reqs.exceptions.RequestException as error: print(f"Request fehlgeschlagen: {error}") # Request fehlgeschlagen: 403 Client Error: Forbidden for url: https://v6.exchangerate-api.com/v6/None/pair/EUR/USD -> sollte nicht gezeigt werden!!! return None diff --git a/src/argus/domain/validation.py b/src/argus/domain/validation.py index 16879a7..1892f2c 100644 --- a/src/argus/domain/validation.py +++ b/src/argus/domain/validation.py @@ -211,3 +211,19 @@ def is_valid_op(op: str) -> bool: Return: bool - True if the operation is valid, otherwise False """ return op in VALID_OPS + + +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): + return resp + + return None diff --git a/src/argus/gui/app.py b/src/argus/gui/app.py index 02f4c1f..8009d7c 100644 --- a/src/argus/gui/app.py +++ b/src/argus/gui/app.py @@ -1,10 +1,14 @@ import tkinter as tk -import pandas as pd +from datetime import date from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg -from argus.services.market_data_service import prepare_trend_analysis -from argus.services.calculator_service import calc, check_op -from argus.services.conversion_service import convert, check_currency -from argus.domain.validation import parse_amount +from matplotlib.figure import Figure +from argus.services.trend_analysis_service import prepare_trend_analysis +from legacy.services.calculator_service import calc, check_op +from argus.services.market_data_service import convert +from argus.domain.validation import parse_amount, check_currency +from argus.domain.internal_models import DataSource, Instrument, MarketDataRequest + +db = "" def on_close() -> None: @@ -69,7 +73,7 @@ def show_conv() -> None: conv_frame.pack(fill=tk.BOTH, expand=True) -def show_trend() -> None: +def show_trend(db: str) -> 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. @@ -86,9 +90,17 @@ def show_trend() -> None: content.pack(side="top", fill=tk.BOTH, expand=True) if trend_canvas is None: - df = pd.DataFrame() - fig = prepare_trend_analysis(df) - if fig is None: + source = DataSource(name="", provider_kind="") + instrument = Instrument(symbol="EUR/USD", name="EUR - USD", asset_class="fx") + req = MarketDataRequest( + source=source, + instrument=instrument, + timeframe="1d", + start=date(2026, 1, 1), + end=date(2026, 1, 4), + ) + fig = prepare_trend_analysis(db, req) + if not isinstance(fig, Figure): return None fig.set_size_inches(7, 4) @@ -145,6 +157,21 @@ def act_convert() -> None: resp1 = check_currency(curr1.get()) resp2 = check_currency(curr2.get()) amount = parse_amount(amount_e.get()) + source = DataSource(name="Exchange API", provider_kind="ex-api") + instrument = Instrument( + symbol="EUR/USD", + name="EUR - USD", + asset_class="fx", + base_currency=resp1, + quote_currency=resp2, + ) + req = MarketDataRequest( + source=source, + instrument=instrument, + timeframe="1d", + start=date(2026, 1, 1), + end=date(2026, 1, 4), + ) if resp1 is None: result_conv_label.config(text="Please enter a valid currency for 'Currency 1'") @@ -160,7 +187,7 @@ def act_convert() -> None: ) return - response = convert(amount, resp1, resp2) + response = convert(amount, req) if response is None: result_conv_label.config(text="Currency conversion error") @@ -251,11 +278,15 @@ def app() -> None: # Buttons from_menu_calc = tk.Button(menu_frame, text="Calculator", command=show_calc) from_menu_conv = tk.Button(menu_frame, text="Converter", command=show_conv) -from_menu_trend_chart = tk.Button(menu_frame, text="Trend Chart", command=show_trend) +from_menu_trend_chart = tk.Button( + menu_frame, text="Trend Chart", command=lambda: show_trend(db) +) from_sidebar_calc = tk.Button(sidebar, text="Calculator", command=show_calc) from_sidebar_conv = tk.Button(sidebar, text="Converter", command=show_conv) -from_sidebar_trend_chart = tk.Button(sidebar, text="Trend Chart", command=show_trend) +from_sidebar_trend_chart = tk.Button( + sidebar, text="Trend Chart", command=lambda: show_trend(db) +) return_menu = tk.Button(sidebar, text="Back to menu", command=show_menu) click_calculate = tk.Button(calc_frame, text="Calculate", command=act_calculate) diff --git a/src/argus/services/market_data_service.py b/src/argus/services/market_data_service.py index 9328141..aae5bd1 100644 --- a/src/argus/services/market_data_service.py +++ b/src/argus/services/market_data_service.py @@ -1,5 +1,6 @@ from argus.domain.internal_models import MarketDataRequest, MarketDataResponse from argus.clients.yfinance_client import get_timeseries +from argus.clients.exchangerate_client import get_rates from argus.storage.database import read_price_bars, insert_price_bar import pandas as pd @@ -45,3 +46,38 @@ def get_market_data( bars=pd.DataFrame(), message=str(e), ) + + +def get_conv_rate(req: MarketDataRequest) -> 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 = get_rates(req) + + if data is None: + return None + + return float(data["conversion_rate"]) + + +def convert(amount: float, req: MarketDataRequest) -> 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 + """ + + rate = get_conv_rate(req) + if rate is not None: + return amount * rate + return None diff --git a/src/legacy/cli/interface.py b/src/legacy/cli/interface.py index 727adaf..46401f2 100644 --- a/src/legacy/cli/interface.py +++ b/src/legacy/cli/interface.py @@ -1,5 +1,5 @@ -from argus.services.calculator_service import check_op, calc -from argus.services.conversion_service import convert, check_currency +from legacy.services.calculator_service import check_op, calc +from legacy.services.conversion_service import convert, check_currency from argus.domain.validation import parse_amount diff --git a/src/argus/services/calculator_service.py b/src/legacy/services/calculator_service.py similarity index 100% rename from src/argus/services/calculator_service.py rename to src/legacy/services/calculator_service.py diff --git a/src/argus/services/conversion_service.py b/src/legacy/services/conversion_service.py similarity index 84% rename from src/argus/services/conversion_service.py rename to src/legacy/services/conversion_service.py index 0dc3cf0..d4b2e0d 100644 --- a/src/argus/services/conversion_service.py +++ b/src/legacy/services/conversion_service.py @@ -1,4 +1,3 @@ -from argus.clients import exchangerate_client as ex_client from argus.domain.validation import normalize_input_string, is_valid_curr_code @@ -18,7 +17,7 @@ def check_currency(question: str) -> str | None: return None -def get_conv_rate(resp1: str, resp2: str) -> float | None: +def get_conv_rate(resp1, resp2) -> float | None: """ Gets the conversion rate between two currencies. @@ -28,12 +27,12 @@ def get_conv_rate(resp1: str, resp2: str) -> float | None: Return: float or None - the conversion rate if found, otherwise None """ - data = ex_client.get_rates(resp1, resp2) + # data = ex_client.get_rates(req) - if data is None: + if 0 == 0: return None - return float(data["conversion_rate"]) + return None # float(data["conversion_rate"]) def convert(amount: float, resp1: str, resp2: str) -> float | None: diff --git a/tests/test_exchangerate_client.py b/tests/test_exchangerate_client.py index faf0864..e205439 100644 --- a/tests/test_exchangerate_client.py +++ b/tests/test_exchangerate_client.py @@ -1,39 +1,71 @@ import requests as req +import pytest from unittest.mock import Mock +from datetime import date +from argus.domain.internal_models import DataSource, Instrument, MarketDataRequest from argus.clients.exchangerate_client import get_rates, check_error -def test_check_currency_timeout(monkeypatch): +@pytest.fixture +def sample_source(): + return DataSource( + name="Exchange API", provider_kind="ex_client", requires_api_key=False + ) + + +@pytest.fixture +def sample_instrument(): + return Instrument( + symbol="EUR/USD", + name="EUR - USD Rate", + asset_class="fx", + base_currency="EUR", + quote_currency="USD", + ) + + +@pytest.fixture +def sample_request(sample_source, sample_instrument): + return MarketDataRequest( + source=sample_source, + instrument=sample_instrument, + timeframe="", + start=date(2026, 1, 1), + end=date(2026, 1, 1), + ) + + +def test_check_currency_timeout(monkeypatch, sample_request): def test_get_resp(url, timeout): raise req.exceptions.Timeout() monkeypatch.setattr("requests.get", test_get_resp) - data = get_rates("EUR", "USD") + data = get_rates(sample_request) assert data is None -def test_check_currency_connection_error(monkeypatch): +def test_check_currency_connection_error(monkeypatch, sample_request): def test_get_resp(url, timeout): raise req.exceptions.ConnectionError() monkeypatch.setattr("requests.get", test_get_resp) - data = get_rates("EUR", "USD") + data = get_rates(sample_request) assert data is None -def test_check_currency_request_exception(monkeypatch): +def test_check_currency_request_exception(monkeypatch, sample_request): def test_get_resp(url, timeout): raise req.exceptions.RequestException("Testfehler") monkeypatch.setattr("requests.get", test_get_resp) - data = get_rates("EUR", "USD") + data = get_rates(sample_request) assert data is None -def test_check_currency_value_error(monkeypatch): +def test_check_currency_value_error(monkeypatch, sample_request): test_resp = Mock() test_resp.raise_for_status.return_value = None test_resp.json.side_effect = ValueError("Ungültige JSON-Antwort") @@ -43,11 +75,11 @@ def test_get_resp(url, timeout): monkeypatch.setattr("requests.get", test_get_resp) - data = get_rates("EUR", "USD") + data = get_rates(sample_request) assert data is None -def test_check_currency_key_error(monkeypatch): +def test_check_currency_key_error(monkeypatch, sample_request): test_resp = Mock() test_resp.raise_for_status.return_value = None test_resp.json.return_value = { @@ -61,11 +93,11 @@ def test_get_resp(url, timeout): monkeypatch.setattr("requests.get", test_get_resp) - data = get_rates("EUR", "USD") + data = get_rates(sample_request) assert data is None -def test_check_currency_valid(monkeypatch): +def test_check_currency_valid(monkeypatch, sample_request): test_resp = Mock() test_resp.raise_for_status.return_value = None test_resp.json.return_value = { @@ -79,11 +111,11 @@ def test_get_resp(url, timeout): monkeypatch.setattr("requests.get", test_get_resp) - data = get_rates("EUR", "USD") + data = get_rates(sample_request) assert data == {"result": "success", "error_type": "", "conversion_rate": 1.2} -def test_check_currency_invalid(monkeypatch): +def test_check_currency_invalid(monkeypatch, sample_request): test_resp = Mock() test_resp.raise_for_status.return_value = None test_resp.json.return_value = { @@ -97,7 +129,7 @@ def test_get_resp(url, timeout): monkeypatch.setattr("requests.get", test_get_resp) - data = get_rates("EUR", "USD") + data = get_rates(sample_request) assert data is None