Skip to content
Closed
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
File renamed without changes.
15 changes: 9 additions & 6 deletions src/argus/clients/exchangerate_client.py
Original file line number Diff line number Diff line change
@@ -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.

Expand All @@ -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
Expand Down
16 changes: 16 additions & 0 deletions src/argus/domain/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
55 changes: 43 additions & 12 deletions src/argus/gui/app.py
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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.
Expand All @@ -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")
Comment thread
BytecodeBrewer marked this conversation as resolved.
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)

Expand Down Expand Up @@ -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'")
Expand All @@ -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")
Expand Down Expand Up @@ -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)
Expand Down
36 changes: 36 additions & 0 deletions src/argus/services/market_data_service.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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
4 changes: 2 additions & 2 deletions src/legacy/cli/interface.py
Original file line number Diff line number Diff line change
@@ -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


Expand Down
Original file line number Diff line number Diff line change
@@ -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


Expand All @@ -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.

Expand All @@ -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
Comment thread
BytecodeBrewer marked this conversation as resolved.

return float(data["conversion_rate"])
return None # float(data["conversion_rate"])


def convert(amount: float, resp1: str, resp2: str) -> float | None:
Expand Down
60 changes: 46 additions & 14 deletions tests/test_exchangerate_client.py
Original file line number Diff line number Diff line change
@@ -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")
Expand All @@ -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 = {
Expand All @@ -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 = {
Expand All @@ -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 = {
Expand All @@ -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


Expand Down
Loading