mirror of
https://github.com/Ichinga-Samuel/aiomql.git
synced 2026-08-20 15:28:08 +00:00
v4.0.17 no-backtest
This commit is contained in:
@@ -1,224 +0,0 @@
|
||||
"""Comprehensive tests for the percentages module.
|
||||
|
||||
Tests cover:
|
||||
- get_price_diff_pct
|
||||
- get_price_in_range_pct
|
||||
- get_price_at_pct
|
||||
- extend_interval_by_percentage
|
||||
- get_price_change_pct
|
||||
- increase_value_by_pct
|
||||
- decrease_value_by_pct
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from aiomql.utils.price_utils import (
|
||||
get_price_diff_pct,
|
||||
get_price_in_range_pct,
|
||||
get_price_at_pct,
|
||||
extend_interval_by_percentage,
|
||||
get_price_change_pct,
|
||||
increase_value_by_pct,
|
||||
decrease_value_by_pct
|
||||
)
|
||||
|
||||
|
||||
class TestCalculatePercentageDifference:
|
||||
"""Tests for get_price_diff_pct function."""
|
||||
|
||||
def test_equal_values(self):
|
||||
"""Test equal values returns 0."""
|
||||
assert get_price_diff_pct(50, 50) == 0.0
|
||||
assert get_price_diff_pct(100, 100) == 0.0
|
||||
|
||||
def test_different_values(self):
|
||||
"""Test different values returns correct percentage."""
|
||||
result = get_price_diff_pct(100, 110)
|
||||
assert abs(result - 9.523809523809524) < 0.0001
|
||||
|
||||
def test_large_difference(self):
|
||||
"""Test large difference."""
|
||||
result = get_price_diff_pct(200, 100)
|
||||
assert abs(result - 66.66666666666666) < 0.0001
|
||||
|
||||
def test_order_independent(self):
|
||||
"""Test result is same regardless of order."""
|
||||
result1 = get_price_diff_pct(100, 200)
|
||||
result2 = get_price_diff_pct(200, 100)
|
||||
assert result1 == result2
|
||||
|
||||
def test_decimal_values(self):
|
||||
"""Test with decimal values."""
|
||||
result = get_price_diff_pct(1.5, 1.0)
|
||||
assert result > 0
|
||||
|
||||
|
||||
class TestCalculatePercentagePosition:
|
||||
"""Tests for get_price_in_range_pct function."""
|
||||
|
||||
def test_value_at_start(self):
|
||||
"""Test value at start returns 0%."""
|
||||
assert get_price_in_range_pct(0, 100, 0) == 0.0
|
||||
assert get_price_in_range_pct(10, 20, 10) == 0.0
|
||||
|
||||
def test_value_at_end(self):
|
||||
"""Test value at end returns 100%."""
|
||||
assert get_price_in_range_pct(0, 100, 100) == 100.0
|
||||
assert get_price_in_range_pct(10, 20, 20) == 100.0
|
||||
|
||||
def test_value_in_middle(self):
|
||||
"""Test value in middle returns 50%."""
|
||||
assert get_price_in_range_pct(0, 100, 50) == 50.0
|
||||
assert get_price_in_range_pct(10, 20, 15) == 50.0
|
||||
|
||||
def test_quarter_position(self):
|
||||
"""Test 25% position."""
|
||||
assert get_price_in_range_pct(0, 100, 25) == 25.0
|
||||
|
||||
def test_value_beyond_end(self):
|
||||
"""Test value beyond end returns > 100%."""
|
||||
result = get_price_in_range_pct(0, 100, 150)
|
||||
assert result == 150.0
|
||||
|
||||
def test_value_before_start(self):
|
||||
"""Test value before start returns negative."""
|
||||
result = get_price_in_range_pct(0, 100, -50)
|
||||
assert result == -50.0
|
||||
|
||||
|
||||
class TestCalculateValueAtPercentage:
|
||||
"""Tests for get_price_at_pct function."""
|
||||
|
||||
def test_zero_percent(self):
|
||||
"""Test 0% returns start value."""
|
||||
assert get_price_at_pct(0, 100, 0) == 0.0
|
||||
assert get_price_at_pct(10, 20, 0) == 10.0
|
||||
|
||||
def test_hundred_percent(self):
|
||||
"""Test 100% returns end value."""
|
||||
assert get_price_at_pct(0, 100, 100) == 100.0
|
||||
assert get_price_at_pct(10, 20, 100) == 20.0
|
||||
|
||||
def test_fifty_percent(self):
|
||||
"""Test 50% returns middle value."""
|
||||
assert get_price_at_pct(0, 100, 50) == 50.0
|
||||
assert get_price_at_pct(10, 20, 50) == 15.0
|
||||
|
||||
def test_quarter_percent(self):
|
||||
"""Test 25% position."""
|
||||
assert get_price_at_pct(0, 200, 25) == 50.0
|
||||
|
||||
def test_beyond_hundred_percent(self):
|
||||
"""Test > 100% extends beyond end."""
|
||||
assert get_price_at_pct(0, 100, 150) == 150.0
|
||||
|
||||
|
||||
class TestExtendIntervalByPercentage:
|
||||
"""Tests for extend_interval_by_percentage function."""
|
||||
|
||||
def test_fifty_percent_extension(self):
|
||||
"""Test 50% extension."""
|
||||
assert extend_interval_by_percentage(0, 100, 50) == 150.0
|
||||
|
||||
def test_hundred_percent_extension(self):
|
||||
"""Test 100% extension (doubles interval)."""
|
||||
assert extend_interval_by_percentage(10, 20, 100) == 30.0
|
||||
|
||||
def test_twenty_percent_extension(self):
|
||||
"""Test 20% extension."""
|
||||
assert extend_interval_by_percentage(0, 50, 20) == 60.0
|
||||
|
||||
def test_zero_percent_extension(self):
|
||||
"""Test 0% extension returns original end."""
|
||||
assert extend_interval_by_percentage(0, 100, 0) == 100.0
|
||||
|
||||
def test_small_interval(self):
|
||||
"""Test with small interval."""
|
||||
result = extend_interval_by_percentage(1.0, 1.1, 50)
|
||||
assert abs(result - 1.15) < 0.0001
|
||||
|
||||
|
||||
class TestCalculatePercentageChange:
|
||||
"""Tests for get_price_change_pct function."""
|
||||
|
||||
def test_no_change(self):
|
||||
"""Test no change returns 0%."""
|
||||
assert get_price_change_pct(50, 50) == 0.0
|
||||
assert get_price_change_pct(100, 100) == 0.0
|
||||
|
||||
def test_positive_change(self):
|
||||
"""Test positive change (increase)."""
|
||||
assert get_price_change_pct(100, 150) == 50.0
|
||||
assert get_price_change_pct(100, 200) == 100.0
|
||||
|
||||
def test_negative_change(self):
|
||||
"""Test negative change (decrease)."""
|
||||
assert get_price_change_pct(200, 100) == -50.0
|
||||
assert get_price_change_pct(100, 50) == -50.0
|
||||
|
||||
def test_double_value(self):
|
||||
"""Test doubling returns 100%."""
|
||||
assert get_price_change_pct(50, 100) == 100.0
|
||||
|
||||
def test_half_value(self):
|
||||
"""Test halving returns -50%."""
|
||||
assert get_price_change_pct(100, 50) == -50.0
|
||||
|
||||
|
||||
class TestIncreaseByPercentage:
|
||||
"""Tests for increase_value_by_pct function."""
|
||||
|
||||
def test_ten_percent_increase(self):
|
||||
"""Test 10% increase."""
|
||||
result = increase_value_by_pct(100, 10)
|
||||
assert result == 110.0
|
||||
|
||||
def test_twenty_percent_increase(self):
|
||||
"""Test 20% increase."""
|
||||
assert increase_value_by_pct(50, 20) == 60.0
|
||||
|
||||
def test_fifty_percent_increase(self):
|
||||
"""Test 50% increase."""
|
||||
assert increase_value_by_pct(200, 50) == 300.0
|
||||
|
||||
def test_zero_percent_increase(self):
|
||||
"""Test 0% increase returns original."""
|
||||
assert increase_value_by_pct(100, 0) == 100.0
|
||||
|
||||
def test_hundred_percent_increase(self):
|
||||
"""Test 100% increase doubles value."""
|
||||
assert increase_value_by_pct(50, 100) == 100.0
|
||||
|
||||
def test_decimal_value(self):
|
||||
"""Test with decimal value."""
|
||||
result = increase_value_by_pct(1.1000, 10)
|
||||
assert abs(result - 1.21) < 0.0001
|
||||
|
||||
|
||||
class TestDecreaseByPercentage:
|
||||
"""Tests for decrease_value_by_pct function."""
|
||||
|
||||
def test_ten_percent_decrease(self):
|
||||
"""Test 10% decrease."""
|
||||
assert decrease_value_by_pct(100, 10) == 90.0
|
||||
|
||||
def test_twenty_percent_decrease(self):
|
||||
"""Test 20% decrease."""
|
||||
assert decrease_value_by_pct(50, 20) == 40.0
|
||||
|
||||
def test_fifty_percent_decrease(self):
|
||||
"""Test 50% decrease."""
|
||||
assert decrease_value_by_pct(200, 50) == 100.0
|
||||
|
||||
def test_zero_percent_decrease(self):
|
||||
"""Test 0% decrease returns original."""
|
||||
assert decrease_value_by_pct(100, 0) == 100.0
|
||||
|
||||
def test_hundred_percent_decrease(self):
|
||||
"""Test 100% decrease returns 0."""
|
||||
assert decrease_value_by_pct(100, 100) == 0.0
|
||||
|
||||
def test_decimal_value(self):
|
||||
"""Test with decimal value."""
|
||||
result = decrease_value_by_pct(1.1000, 10)
|
||||
assert abs(result - 0.99) < 0.0001
|
||||
@@ -0,0 +1,281 @@
|
||||
"""Comprehensive tests for the price_utils module.
|
||||
|
||||
Tests cover all 8 pure utility functions:
|
||||
- get_price_diff_pct
|
||||
- get_price_in_range_pct
|
||||
- get_price_at_pct
|
||||
- extend_range_by_pct
|
||||
- get_price_change_pct
|
||||
- increase_value_by_pct
|
||||
- decrease_value_by_pct
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from aiomql.utils.price_utils import (
|
||||
get_price_diff_pct,
|
||||
get_price_in_range_pct,
|
||||
get_price_at_pct,
|
||||
extend_range_by_pct,
|
||||
get_price_change_pct,
|
||||
increase_value_by_pct,
|
||||
decrease_value_by_pct,
|
||||
)
|
||||
|
||||
|
||||
class TestGetPriceDiffPct:
|
||||
"""Tests for get_price_diff_pct function."""
|
||||
|
||||
def test_equal_values(self):
|
||||
"""Test zero difference when values are equal."""
|
||||
assert get_price_diff_pct(50, 50) == 0.0
|
||||
|
||||
def test_small_difference(self):
|
||||
"""Test small percentage difference."""
|
||||
result = get_price_diff_pct(100, 110)
|
||||
assert pytest.approx(result, rel=1e-6) == 9.523809523809524
|
||||
|
||||
def test_large_difference(self):
|
||||
"""Test large percentage difference."""
|
||||
result = get_price_diff_pct(200, 100)
|
||||
assert pytest.approx(result, rel=1e-6) == 66.66666666666666
|
||||
|
||||
def test_order_does_not_matter(self):
|
||||
"""Test that argument order gives same result (symmetric)."""
|
||||
assert get_price_diff_pct(100, 110) == get_price_diff_pct(110, 100)
|
||||
|
||||
def test_with_decimals(self):
|
||||
"""Test with decimal values (common in forex pricing)."""
|
||||
result = get_price_diff_pct(1.1000, 1.1050)
|
||||
assert result > 0
|
||||
|
||||
def test_very_close_values(self):
|
||||
"""Test with very close values."""
|
||||
result = get_price_diff_pct(1.10000, 1.10001)
|
||||
assert result > 0
|
||||
assert result < 0.01 # Very small percentage
|
||||
|
||||
|
||||
class TestGetPriceInRangePct:
|
||||
"""Tests for get_price_in_range_pct function."""
|
||||
|
||||
def test_midpoint(self):
|
||||
"""Test value at midpoint returns 50%."""
|
||||
assert get_price_in_range_pct(0, 100, 50) == 50.0
|
||||
|
||||
def test_start_value(self):
|
||||
"""Test value at start returns 0%."""
|
||||
assert get_price_in_range_pct(0, 100, 0) == 0.0
|
||||
|
||||
def test_end_value(self):
|
||||
"""Test value at end returns 100%."""
|
||||
assert get_price_in_range_pct(0, 100, 100) == 100.0
|
||||
|
||||
def test_quarter(self):
|
||||
"""Test value at 25%."""
|
||||
assert get_price_in_range_pct(0, 100, 25) == 25.0
|
||||
|
||||
def test_offset_range(self):
|
||||
"""Test with non-zero start."""
|
||||
assert get_price_in_range_pct(10, 20, 15) == 50.0
|
||||
|
||||
def test_beyond_range(self):
|
||||
"""Test value beyond the range returns > 100%."""
|
||||
result = get_price_in_range_pct(0, 100, 150)
|
||||
assert result == 150.0
|
||||
|
||||
def test_below_range(self):
|
||||
"""Test value below the range returns negative."""
|
||||
result = get_price_in_range_pct(10, 20, 5)
|
||||
assert result == -50.0
|
||||
|
||||
def test_forex_prices(self):
|
||||
"""Test with realistic forex price ranges."""
|
||||
# Price at 80% of move from 1.1000 to 1.1100
|
||||
result = get_price_in_range_pct(1.1000, 1.1100, 1.1080)
|
||||
assert pytest.approx(result, rel=1e-6) == 80.0
|
||||
|
||||
|
||||
class TestGetPriceAtPct:
|
||||
"""Tests for get_price_at_pct function."""
|
||||
|
||||
def test_zero_percent(self):
|
||||
"""Test 0% returns start value."""
|
||||
assert get_price_at_pct(0, 100, 0) == 0.0
|
||||
|
||||
def test_hundred_percent(self):
|
||||
"""Test 100% returns end value."""
|
||||
assert get_price_at_pct(0, 100, 100) == 100.0
|
||||
|
||||
def test_fifty_percent(self):
|
||||
"""Test 50% returns midpoint."""
|
||||
assert get_price_at_pct(0, 100, 50) == 50.0
|
||||
|
||||
def test_offset_range(self):
|
||||
"""Test with non-zero start."""
|
||||
assert get_price_at_pct(10, 20, 50) == 15.0
|
||||
|
||||
def test_twenty_five_percent(self):
|
||||
"""Test 25%."""
|
||||
assert get_price_at_pct(0, 200, 25) == 50.0
|
||||
|
||||
def test_over_hundred_percent(self):
|
||||
"""Test beyond 100% extends past end."""
|
||||
result = get_price_at_pct(0, 100, 150)
|
||||
assert result == 150.0
|
||||
|
||||
def test_inverse_of_get_price_in_range_pct(self):
|
||||
"""Test that get_price_at_pct is the inverse of get_price_in_range_pct."""
|
||||
start, end = 1.1000, 1.1100
|
||||
pct = 75.0
|
||||
value = get_price_at_pct(start, end, pct)
|
||||
recovered_pct = get_price_in_range_pct(start, end, value)
|
||||
assert pytest.approx(recovered_pct, rel=1e-6) == pct
|
||||
|
||||
|
||||
class TestExtendRangeByPct:
|
||||
"""Tests for extend_range_by_pct function."""
|
||||
|
||||
def test_extend_by_fifty_percent(self):
|
||||
"""Test extending range by 50%."""
|
||||
assert extend_range_by_pct(0, 100, 50) == 150.0
|
||||
|
||||
def test_extend_by_hundred_percent(self):
|
||||
"""Test extending range by 100% (doubles the span beyond end)."""
|
||||
assert extend_range_by_pct(10, 20, 100) == 30.0
|
||||
|
||||
def test_extend_by_twenty_percent(self):
|
||||
"""Test extending range by 20%."""
|
||||
assert extend_range_by_pct(0, 50, 20) == 60.0
|
||||
|
||||
def test_extend_by_zero(self):
|
||||
"""Test extending by 0% returns original end."""
|
||||
assert extend_range_by_pct(0, 100, 0) == 100.0
|
||||
|
||||
def test_forex_take_profit_extension(self):
|
||||
"""Test realistic forex TP extension scenario."""
|
||||
# Extend TP from 1.1100 (opened at 1.1000) by 20%
|
||||
new_tp = extend_range_by_pct(1.1000, 1.1100, 20)
|
||||
expected = 1.1100 + (0.0100 * 0.20) # 1.1120
|
||||
assert pytest.approx(new_tp, rel=1e-6) == expected
|
||||
|
||||
def test_small_extension(self):
|
||||
"""Test small percentage extension."""
|
||||
result = extend_range_by_pct(100, 200, 10)
|
||||
assert result == 210.0
|
||||
|
||||
|
||||
class TestGetPriceChangePct:
|
||||
"""Tests for get_price_change_pct function."""
|
||||
|
||||
def test_no_change(self):
|
||||
"""Test zero change."""
|
||||
assert get_price_change_pct(50, 50) == 0.0
|
||||
|
||||
def test_increase(self):
|
||||
"""Test positive price change."""
|
||||
assert get_price_change_pct(100, 150) == 50.0
|
||||
|
||||
def test_decrease(self):
|
||||
"""Test negative price change."""
|
||||
assert get_price_change_pct(200, 100) == -50.0
|
||||
|
||||
def test_double(self):
|
||||
"""Test 100% increase (doubling)."""
|
||||
assert get_price_change_pct(100, 200) == 100.0
|
||||
|
||||
def test_small_change(self):
|
||||
"""Test small forex-like price change."""
|
||||
result = get_price_change_pct(1.1000, 1.1010)
|
||||
assert pytest.approx(result, abs=0.01) == pytest.approx(0.0909, abs=0.01)
|
||||
|
||||
def test_negative_values(self):
|
||||
"""Test with signed values (e.g. profit going more negative)."""
|
||||
result = get_price_change_pct(-100, -50)
|
||||
assert result == -50.0
|
||||
|
||||
|
||||
class TestIncreaseValueByPct:
|
||||
"""Tests for increase_value_by_pct function."""
|
||||
|
||||
def test_increase_by_ten_percent(self):
|
||||
"""Test 10% increase."""
|
||||
assert round(increase_value_by_pct(100, 10), 2) == 110.0
|
||||
|
||||
def test_increase_by_twenty_percent(self):
|
||||
"""Test 20% increase."""
|
||||
assert increase_value_by_pct(50, 20) == 60.0
|
||||
|
||||
def test_increase_by_fifty_percent(self):
|
||||
"""Test 50% increase."""
|
||||
assert increase_value_by_pct(200, 50) == 300.0
|
||||
|
||||
def test_increase_by_zero(self):
|
||||
"""Test 0% increase returns original value."""
|
||||
assert increase_value_by_pct(100, 0) == 100.0
|
||||
|
||||
def test_increase_by_hundred_percent(self):
|
||||
"""Test 100% increase doubles the value."""
|
||||
assert increase_value_by_pct(100, 100) == 200.0
|
||||
|
||||
def test_increase_with_decimals(self):
|
||||
"""Test increase with decimal input."""
|
||||
result = increase_value_by_pct(1.1000, 5)
|
||||
assert pytest.approx(result, rel=1e-6) == 1.155
|
||||
|
||||
|
||||
class TestDecreaseValueByPct:
|
||||
"""Tests for decrease_value_by_pct function."""
|
||||
|
||||
def test_decrease_by_ten_percent(self):
|
||||
"""Test 10% decrease."""
|
||||
assert decrease_value_by_pct(100, 10) == 90.0
|
||||
|
||||
def test_decrease_by_twenty_percent(self):
|
||||
"""Test 20% decrease."""
|
||||
assert decrease_value_by_pct(50, 20) == 40.0
|
||||
|
||||
def test_decrease_by_fifty_percent(self):
|
||||
"""Test 50% decrease halves the value."""
|
||||
assert decrease_value_by_pct(200, 50) == 100.0
|
||||
|
||||
def test_decrease_by_zero(self):
|
||||
"""Test 0% decrease returns original value."""
|
||||
assert decrease_value_by_pct(100, 0) == 100.0
|
||||
|
||||
def test_decrease_by_hundred_percent(self):
|
||||
"""Test 100% decrease returns zero."""
|
||||
assert decrease_value_by_pct(100, 100) == 0.0
|
||||
|
||||
def test_decrease_with_decimals(self):
|
||||
"""Test decrease with decimal input."""
|
||||
result = decrease_value_by_pct(1.1000, 5)
|
||||
assert pytest.approx(result, rel=1e-6) == 1.045
|
||||
|
||||
|
||||
class TestFunctionInteractions:
|
||||
"""Tests verifying relationships between price utility functions."""
|
||||
|
||||
def test_increase_then_decrease_returns_original(self):
|
||||
"""Test that increase then decrease by same rate does NOT return original
|
||||
(this is expected due to compounding)."""
|
||||
original = 100.0
|
||||
increased = increase_value_by_pct(original, 10)
|
||||
result = decrease_value_by_pct(increased, 10)
|
||||
# 100 * 1.1 * 0.9 = 99.0, NOT 100 (compounding effect)
|
||||
assert pytest.approx(result, rel=1e-6) == 99.0
|
||||
|
||||
def test_extend_range_consistent_with_range_pct(self):
|
||||
"""Test that extended range position is beyond 100%."""
|
||||
start, end = 0, 100
|
||||
extended = extend_range_by_pct(start, end, 50)
|
||||
pct = get_price_in_range_pct(start, end, extended)
|
||||
assert pct == 150.0
|
||||
|
||||
def test_price_change_consistent_with_increase(self):
|
||||
"""Test that increase_value_by_pct result matches get_price_change_pct."""
|
||||
original = 100.0
|
||||
rate = 25.0
|
||||
increased = increase_value_by_pct(original, rate)
|
||||
change = get_price_change_pct(original, increased)
|
||||
assert pytest.approx(change, rel=1e-6) == rate
|
||||
@@ -12,7 +12,7 @@ Tests cover:
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock, AsyncMock
|
||||
from unittest.mock import patch
|
||||
|
||||
from aiomql.utils.utils import (
|
||||
dict_to_string,
|
||||
@@ -62,29 +62,43 @@ class TestDictToString:
|
||||
assert "float: 3.14" in result
|
||||
assert "bool: True" in result
|
||||
|
||||
def test_multi_false_uses_comma_separator(self):
|
||||
"""Test that multi=False uses comma-space separator."""
|
||||
result = dict_to_string({"a": 1, "b": 2}, multi=False)
|
||||
assert "\n" not in result
|
||||
assert ", " in result
|
||||
|
||||
def test_single_item_no_separator(self):
|
||||
"""Test single item has no separator character."""
|
||||
result = dict_to_string({"key": "val"})
|
||||
assert "," not in result
|
||||
assert "\n" not in result
|
||||
|
||||
|
||||
class TestBackoffDecorator:
|
||||
"""Tests for backoff_decorator."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_successful_call_no_retry(self):
|
||||
"""Test successful call does not retry."""
|
||||
call_count = 0
|
||||
|
||||
|
||||
@backoff_decorator
|
||||
async def success_func():
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
return "success"
|
||||
|
||||
|
||||
result = await success_func()
|
||||
|
||||
|
||||
assert result == "success"
|
||||
assert call_count == 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_retry_on_exception(self):
|
||||
"""Test retries on exception."""
|
||||
"""Test retries on exception until success."""
|
||||
call_count = 0
|
||||
|
||||
|
||||
@backoff_decorator(max_retries=3)
|
||||
async def failing_func():
|
||||
nonlocal call_count
|
||||
@@ -92,152 +106,245 @@ class TestBackoffDecorator:
|
||||
if call_count < 3:
|
||||
raise ValueError("Test error")
|
||||
return "success"
|
||||
|
||||
with patch("aiomql.utils.utils.Config") as mock_config:
|
||||
mock_config.return_value.mode = "backtest" # Skip sleep
|
||||
result = await failing_func()
|
||||
|
||||
|
||||
result = await failing_func()
|
||||
|
||||
assert result == "success"
|
||||
assert call_count == 3
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_max_retries_exceeded(self):
|
||||
"""Test raises after max retries exceeded."""
|
||||
call_count = 0
|
||||
|
||||
|
||||
@backoff_decorator(max_retries=2)
|
||||
async def always_fails():
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
raise ValueError("Always fails")
|
||||
|
||||
with patch("aiomql.utils.utils.Config") as mock_config:
|
||||
mock_config.return_value.mode = "backtest"
|
||||
with pytest.raises(ValueError, match="Always fails"):
|
||||
await always_fails()
|
||||
|
||||
|
||||
with pytest.raises(ValueError, match="Always fails"):
|
||||
await always_fails()
|
||||
|
||||
assert call_count == 3 # Initial + 2 retries
|
||||
|
||||
async def test_backoff_delay_in_live_mode(self):
|
||||
"""Test backoff delay applied in live mode."""
|
||||
call_count = 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_max_retries_logs_error(self):
|
||||
"""Test logs error when max retries exceeded."""
|
||||
@backoff_decorator(max_retries=1)
|
||||
async def failing_func():
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
if call_count < 2:
|
||||
raise ValueError("Test error")
|
||||
return "success"
|
||||
|
||||
with patch("aiomql.utils.utils.Config") as mock_config:
|
||||
mock_config.return_value.mode = "live"
|
||||
with patch("aiomql.utils.utils.asyncio.sleep", new_callable=AsyncMock) as mock_sleep:
|
||||
result = await failing_func()
|
||||
mock_sleep.assert_called_once()
|
||||
async def always_fails():
|
||||
raise ValueError("Test error")
|
||||
|
||||
with patch("aiomql.utils.utils.logger") as mock_logger:
|
||||
with pytest.raises(ValueError):
|
||||
await always_fails()
|
||||
mock_logger.error.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_decorator_without_parentheses(self):
|
||||
"""Test decorator can be used without parentheses."""
|
||||
@backoff_decorator
|
||||
async def simple_func():
|
||||
return "result"
|
||||
|
||||
|
||||
result = await simple_func()
|
||||
assert result == "result"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_decorator_with_parentheses(self):
|
||||
"""Test decorator can be used with parentheses."""
|
||||
@backoff_decorator()
|
||||
async def simple_func():
|
||||
return "result"
|
||||
|
||||
|
||||
result = await simple_func()
|
||||
assert result == "result"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_passes_args_and_kwargs(self):
|
||||
"""Test decorated function receives args and kwargs correctly."""
|
||||
@backoff_decorator
|
||||
async def add(a, b, c=0):
|
||||
return a + b + c
|
||||
|
||||
result = await add(1, 2, c=3)
|
||||
assert result == 6
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_retries_reset_on_success(self):
|
||||
"""Test retries counter resets after a successful call."""
|
||||
call_count = 0
|
||||
|
||||
@backoff_decorator(max_retries=2)
|
||||
async def intermittent_func():
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
if call_count == 1:
|
||||
raise ValueError("First call fails")
|
||||
return "success"
|
||||
|
||||
# First call succeeds after 1 retry
|
||||
result = await intermittent_func()
|
||||
assert result == "success"
|
||||
|
||||
# Reset call_count for second invocation
|
||||
call_count = 10 # Won't fail since count != 1
|
||||
|
||||
# Second call should also work (retries were reset)
|
||||
result = await intermittent_func()
|
||||
assert result == "success"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_preserves_function_name(self):
|
||||
"""Test decorator preserves original function name via @wraps."""
|
||||
@backoff_decorator
|
||||
async def my_function():
|
||||
return True
|
||||
|
||||
assert my_function.__name__ == "my_function"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_custom_max_retries(self):
|
||||
"""Test custom max_retries value is respected."""
|
||||
call_count = 0
|
||||
|
||||
@backoff_decorator(max_retries=5)
|
||||
async def failing_func():
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
if call_count < 5:
|
||||
raise ValueError("Fail")
|
||||
return "success"
|
||||
|
||||
result = await failing_func()
|
||||
assert result == "success"
|
||||
assert call_count == 5
|
||||
|
||||
|
||||
class TestErrorHandler:
|
||||
"""Tests for error_handler async decorator."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_successful_call(self):
|
||||
"""Test successful call returns result."""
|
||||
@error_handler
|
||||
async def success_func():
|
||||
return "success"
|
||||
|
||||
|
||||
result = await success_func()
|
||||
assert result == "success"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_exception_returns_response(self):
|
||||
"""Test exception returns configured response."""
|
||||
@error_handler(response="default")
|
||||
async def failing_func():
|
||||
raise ValueError("Test error")
|
||||
|
||||
|
||||
with patch("aiomql.utils.utils.logger"):
|
||||
result = await failing_func()
|
||||
|
||||
|
||||
assert result == "default"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_exception_returns_none_by_default(self):
|
||||
"""Test exception returns None by default."""
|
||||
@error_handler
|
||||
async def failing_func():
|
||||
raise ValueError("Test error")
|
||||
|
||||
|
||||
with patch("aiomql.utils.utils.logger"):
|
||||
result = await failing_func()
|
||||
|
||||
|
||||
assert result is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_custom_exception_type(self):
|
||||
"""Test catches only specified exception type."""
|
||||
@error_handler(exe=ValueError, response="caught")
|
||||
async def failing_func():
|
||||
raise ValueError("Test error")
|
||||
|
||||
|
||||
with patch("aiomql.utils.utils.logger"):
|
||||
result = await failing_func()
|
||||
|
||||
|
||||
assert result == "caught"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unmatched_exception_propagates(self):
|
||||
"""Test unmatched exception propagates."""
|
||||
@error_handler(exe=ValueError, response="caught")
|
||||
async def failing_func():
|
||||
raise TypeError("Wrong type")
|
||||
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
await failing_func()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_logs_error_message(self):
|
||||
"""Test logs error message."""
|
||||
@error_handler
|
||||
async def failing_func():
|
||||
raise ValueError("Test error")
|
||||
|
||||
|
||||
with patch("aiomql.utils.utils.logger") as mock_logger:
|
||||
await failing_func()
|
||||
mock_logger.error.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_custom_error_message(self):
|
||||
"""Test custom error message is logged."""
|
||||
@error_handler(msg="Custom error message")
|
||||
async def failing_func():
|
||||
raise ValueError("Test error")
|
||||
|
||||
|
||||
with patch("aiomql.utils.utils.logger") as mock_logger:
|
||||
await failing_func()
|
||||
mock_logger.error.assert_called_once_with("Custom error message")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_default_error_message_format(self):
|
||||
"""Test default error message includes function name and error."""
|
||||
@error_handler
|
||||
async def my_func():
|
||||
raise ValueError("specific error")
|
||||
|
||||
with patch("aiomql.utils.utils.logger") as mock_logger:
|
||||
await my_func()
|
||||
call_args = mock_logger.error.call_args[0][0]
|
||||
assert "my_func" in call_args
|
||||
assert "specific error" in call_args
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_log_error_msg_false(self):
|
||||
"""Test no logging when log_error_msg is False."""
|
||||
@error_handler(log_error_msg=False)
|
||||
async def failing_func():
|
||||
raise ValueError("Test error")
|
||||
|
||||
|
||||
with patch("aiomql.utils.utils.logger") as mock_logger:
|
||||
await failing_func()
|
||||
mock_logger.error.assert_not_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_preserves_function_name(self):
|
||||
"""Test decorator preserves original function name via @wraps."""
|
||||
@error_handler
|
||||
async def my_special_func():
|
||||
return True
|
||||
|
||||
assert my_special_func.__name__ == "my_special_func"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_passes_args_and_kwargs(self):
|
||||
"""Test decorated function receives args and kwargs correctly."""
|
||||
@error_handler
|
||||
async def add(a, b, c=0):
|
||||
return a + b + c
|
||||
|
||||
result = await add(1, 2, c=3)
|
||||
assert result == 6
|
||||
|
||||
|
||||
class TestErrorHandlerSync:
|
||||
"""Tests for error_handler_sync decorator."""
|
||||
@@ -247,7 +354,7 @@ class TestErrorHandlerSync:
|
||||
@error_handler_sync
|
||||
def success_func():
|
||||
return "success"
|
||||
|
||||
|
||||
result = success_func()
|
||||
assert result == "success"
|
||||
|
||||
@@ -256,10 +363,10 @@ class TestErrorHandlerSync:
|
||||
@error_handler_sync(response="default")
|
||||
def failing_func():
|
||||
raise ValueError("Test error")
|
||||
|
||||
|
||||
with patch("aiomql.utils.utils.logger"):
|
||||
result = failing_func()
|
||||
|
||||
|
||||
assert result == "default"
|
||||
|
||||
def test_exception_returns_none_by_default(self):
|
||||
@@ -267,10 +374,10 @@ class TestErrorHandlerSync:
|
||||
@error_handler_sync
|
||||
def failing_func():
|
||||
raise ValueError("Test error")
|
||||
|
||||
|
||||
with patch("aiomql.utils.utils.logger"):
|
||||
result = failing_func()
|
||||
|
||||
|
||||
assert result is None
|
||||
|
||||
def test_custom_exception_type(self):
|
||||
@@ -278,10 +385,10 @@ class TestErrorHandlerSync:
|
||||
@error_handler_sync(exe=ValueError, response="caught")
|
||||
def failing_func():
|
||||
raise ValueError("Test error")
|
||||
|
||||
|
||||
with patch("aiomql.utils.utils.logger"):
|
||||
result = failing_func()
|
||||
|
||||
|
||||
assert result == "caught"
|
||||
|
||||
def test_unmatched_exception_propagates(self):
|
||||
@@ -289,7 +396,7 @@ class TestErrorHandlerSync:
|
||||
@error_handler_sync(exe=ValueError)
|
||||
def failing_func():
|
||||
raise TypeError("Wrong type")
|
||||
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
failing_func()
|
||||
|
||||
@@ -298,21 +405,62 @@ class TestErrorHandlerSync:
|
||||
@error_handler_sync
|
||||
def failing_func():
|
||||
raise ValueError("Test error")
|
||||
|
||||
|
||||
with patch("aiomql.utils.utils.logger") as mock_logger:
|
||||
failing_func()
|
||||
mock_logger.error.assert_called_once()
|
||||
|
||||
def test_custom_error_message(self):
|
||||
"""Test custom error message is logged."""
|
||||
@error_handler_sync(msg="Custom sync error")
|
||||
def failing_func():
|
||||
raise ValueError("Test error")
|
||||
|
||||
with patch("aiomql.utils.utils.logger") as mock_logger:
|
||||
failing_func()
|
||||
# error_handler_sync uses: f"Error in {func.__name__}: {msg or err}"
|
||||
call_args = mock_logger.error.call_args[0][0]
|
||||
assert "Custom sync error" in call_args
|
||||
|
||||
def test_default_error_message_format(self):
|
||||
"""Test default error message includes function name and error."""
|
||||
@error_handler_sync
|
||||
def my_sync_func():
|
||||
raise ValueError("specific error")
|
||||
|
||||
with patch("aiomql.utils.utils.logger") as mock_logger:
|
||||
my_sync_func()
|
||||
call_args = mock_logger.error.call_args[0][0]
|
||||
assert "my_sync_func" in call_args
|
||||
assert "specific error" in call_args
|
||||
|
||||
def test_log_error_msg_false(self):
|
||||
"""Test no logging when log_error_msg is False."""
|
||||
@error_handler_sync(log_error_msg=False)
|
||||
def failing_func():
|
||||
raise ValueError("Test error")
|
||||
|
||||
|
||||
with patch("aiomql.utils.utils.logger") as mock_logger:
|
||||
failing_func()
|
||||
mock_logger.error.assert_not_called()
|
||||
|
||||
def test_preserves_function_name(self):
|
||||
"""Test decorator preserves original function name via @wraps."""
|
||||
@error_handler_sync
|
||||
def my_sync_special_func():
|
||||
return True
|
||||
|
||||
assert my_sync_special_func.__name__ == "my_sync_special_func"
|
||||
|
||||
def test_passes_args_and_kwargs(self):
|
||||
"""Test decorated function receives args and kwargs correctly."""
|
||||
@error_handler_sync
|
||||
def add(a, b, c=0):
|
||||
return a + b + c
|
||||
|
||||
result = add(1, 2, c=3)
|
||||
assert result == 6
|
||||
|
||||
|
||||
class TestRoundDown:
|
||||
"""Tests for round_down function."""
|
||||
@@ -337,6 +485,11 @@ class TestRoundDown:
|
||||
assert round_down(3, 5) == 0
|
||||
assert round_down(9, 10) == 0
|
||||
|
||||
def test_round_down_large_number(self):
|
||||
"""Test rounding down large numbers."""
|
||||
assert round_down(997, 100) == 900
|
||||
assert round_down(1050, 1000) == 1000
|
||||
|
||||
|
||||
class TestRoundUp:
|
||||
"""Tests for round_up function."""
|
||||
@@ -361,6 +514,11 @@ class TestRoundUp:
|
||||
assert round_up(1, 5) == 5
|
||||
assert round_up(1, 10) == 10
|
||||
|
||||
def test_round_up_large_number(self):
|
||||
"""Test rounding up large numbers."""
|
||||
assert round_up(901, 100) == 1000
|
||||
assert round_up(1001, 1000) == 2000
|
||||
|
||||
|
||||
class TestRoundOff:
|
||||
"""Tests for round_off function."""
|
||||
@@ -390,78 +548,123 @@ class TestRoundOff:
|
||||
assert round_off(5.5, 1) == 6.0
|
||||
assert round_off(5.5, 1, round_down=True) == 5.0
|
||||
|
||||
def test_small_step_forex_lot(self):
|
||||
"""Test with very small step (forex lot size precision)."""
|
||||
assert round_off(0.0123, 0.01) == 0.02
|
||||
assert round_off(0.0123, 0.01, round_down=True) == 0.01
|
||||
|
||||
def test_volume_step(self):
|
||||
"""Test with volume step (common in trading)."""
|
||||
assert round_off(0.15, 0.1) == 0.2
|
||||
assert round_off(0.15, 0.1, round_down=True) == 0.1
|
||||
|
||||
|
||||
class TestAsyncCache:
|
||||
"""Tests for async_cache decorator."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_caches_result(self):
|
||||
"""Test result is cached."""
|
||||
call_count = 0
|
||||
|
||||
|
||||
@async_cache
|
||||
async def cached_func():
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
return "result"
|
||||
|
||||
|
||||
result1 = await cached_func()
|
||||
result2 = await cached_func()
|
||||
|
||||
|
||||
assert result1 == "result"
|
||||
assert result2 == "result"
|
||||
assert call_count == 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_different_args_different_cache(self):
|
||||
"""Test different args have different cache entries."""
|
||||
call_count = 0
|
||||
|
||||
|
||||
@async_cache
|
||||
async def cached_func(x):
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
return x * 2
|
||||
|
||||
|
||||
result1 = await cached_func(1)
|
||||
result2 = await cached_func(2)
|
||||
result3 = await cached_func(1) # Should be cached
|
||||
|
||||
|
||||
assert result1 == 2
|
||||
assert result2 == 4
|
||||
assert result3 == 2
|
||||
assert call_count == 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_kwargs_in_cache_key(self):
|
||||
"""Test kwargs are included in cache key."""
|
||||
call_count = 0
|
||||
|
||||
|
||||
@async_cache
|
||||
async def cached_func(x, y=1):
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
return x + y
|
||||
|
||||
|
||||
result1 = await cached_func(1, y=2)
|
||||
result2 = await cached_func(1, y=3)
|
||||
result3 = await cached_func(1, y=2) # Should be cached
|
||||
|
||||
|
||||
assert result1 == 3
|
||||
assert result2 == 4
|
||||
assert result3 == 3
|
||||
assert call_count == 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cache_has_lock(self):
|
||||
"""Test cached function has lock attribute."""
|
||||
@async_cache
|
||||
async def cached_func():
|
||||
return "result"
|
||||
|
||||
|
||||
assert hasattr(cached_func, "lock")
|
||||
assert hasattr(cached_func, "cache")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cache_is_dict(self):
|
||||
"""Test cache is a dictionary."""
|
||||
@async_cache
|
||||
async def cached_func():
|
||||
return "result"
|
||||
|
||||
|
||||
assert isinstance(cached_func.cache, dict)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cache_can_be_cleared(self):
|
||||
"""Test cache can be manually cleared."""
|
||||
call_count = 0
|
||||
|
||||
@async_cache
|
||||
async def cached_func():
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
return "result"
|
||||
|
||||
await cached_func()
|
||||
assert call_count == 1
|
||||
|
||||
# Clear cache
|
||||
cached_func.cache.clear()
|
||||
|
||||
# Should call function again
|
||||
await cached_func()
|
||||
assert call_count == 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_preserves_function_name(self):
|
||||
"""Test decorator preserves original function name via @wraps."""
|
||||
@async_cache
|
||||
async def my_cached_func():
|
||||
return True
|
||||
|
||||
assert my_cached_func.__name__ == "my_cached_func"
|
||||
|
||||
Reference in New Issue
Block a user