- Add Pydantic settings with trading mode validation (paper/testnet/live) - Implement Binance USDⓈ-M Futures adapter with hedge mode, isolated margin - Add type definitions for orders, positions, and market data - Create documentation (PLAN.md, ARCHITECTURE.md, SECURITY.md) - Add setup.sh with uv/pip auto-detection for consistent dev environments - Configure Docker multi-stage build and docker-compose services - Add pyproject.toml with all dependencies and tool configs
155 lines
5.1 KiB
Python
155 lines
5.1 KiB
Python
"""Tests for configuration module."""
|
|
|
|
import os
|
|
from decimal import Decimal
|
|
|
|
import pytest
|
|
|
|
from tradefinder.adapters.types import (
|
|
OrderRequest,
|
|
OrderType,
|
|
PositionSide,
|
|
Side,
|
|
)
|
|
|
|
|
|
class TestOrderRequest:
|
|
"""Tests for OrderRequest validation."""
|
|
|
|
def test_valid_limit_order(self) -> None:
|
|
"""Test valid limit order creation."""
|
|
order = OrderRequest(
|
|
symbol="BTCUSDT",
|
|
side=Side.BUY,
|
|
position_side=PositionSide.LONG,
|
|
order_type=OrderType.LIMIT,
|
|
quantity=Decimal("0.001"),
|
|
price=Decimal("50000"),
|
|
)
|
|
order.validate() # Should not raise
|
|
|
|
def test_limit_order_without_price_fails(self) -> None:
|
|
"""Test limit order without price raises error."""
|
|
order = OrderRequest(
|
|
symbol="BTCUSDT",
|
|
side=Side.BUY,
|
|
position_side=PositionSide.LONG,
|
|
order_type=OrderType.LIMIT,
|
|
quantity=Decimal("0.001"),
|
|
price=None,
|
|
)
|
|
with pytest.raises(ValueError, match="Limit orders require a price"):
|
|
order.validate()
|
|
|
|
def test_stop_order_without_stop_price_fails(self) -> None:
|
|
"""Test stop order without stop price raises error."""
|
|
order = OrderRequest(
|
|
symbol="BTCUSDT",
|
|
side=Side.SELL,
|
|
position_side=PositionSide.LONG,
|
|
order_type=OrderType.STOP_MARKET,
|
|
quantity=Decimal("0.001"),
|
|
stop_price=None,
|
|
)
|
|
with pytest.raises(ValueError, match="STOP_MARKET orders require a stop_price"):
|
|
order.validate()
|
|
|
|
def test_valid_stop_market_order(self) -> None:
|
|
"""Test valid stop market order creation."""
|
|
order = OrderRequest(
|
|
symbol="BTCUSDT",
|
|
side=Side.SELL,
|
|
position_side=PositionSide.LONG,
|
|
order_type=OrderType.STOP_MARKET,
|
|
quantity=Decimal("0.001"),
|
|
stop_price=Decimal("48000"),
|
|
)
|
|
order.validate() # Should not raise
|
|
|
|
def test_zero_quantity_fails(self) -> None:
|
|
"""Test zero quantity raises error."""
|
|
order = OrderRequest(
|
|
symbol="BTCUSDT",
|
|
side=Side.BUY,
|
|
position_side=PositionSide.LONG,
|
|
order_type=OrderType.MARKET,
|
|
quantity=Decimal("0"),
|
|
)
|
|
with pytest.raises(ValueError, match="Quantity must be positive"):
|
|
order.validate()
|
|
|
|
def test_negative_quantity_fails(self) -> None:
|
|
"""Test negative quantity raises error."""
|
|
order = OrderRequest(
|
|
symbol="BTCUSDT",
|
|
side=Side.BUY,
|
|
position_side=PositionSide.LONG,
|
|
order_type=OrderType.MARKET,
|
|
quantity=Decimal("-0.001"),
|
|
)
|
|
with pytest.raises(ValueError, match="Quantity must be positive"):
|
|
order.validate()
|
|
|
|
|
|
class TestConfigValidation:
|
|
"""Tests for Settings configuration validation.
|
|
|
|
These tests require environment variables or .env file setup.
|
|
They verify that configuration validation works correctly.
|
|
"""
|
|
|
|
def test_symbols_parsing_from_string(self) -> None:
|
|
"""Test comma-separated symbol parsing."""
|
|
# This tests the validator logic directly
|
|
from tradefinder.core.config import Settings
|
|
|
|
# Simulate what the validator does
|
|
input_str = "BTCUSDT, ETHUSDT, SOLUSDT"
|
|
result = [s.strip().upper() for s in input_str.split(",") if s.strip()]
|
|
assert result == ["BTCUSDT", "ETHUSDT", "SOLUSDT"]
|
|
|
|
def test_valid_timeframes(self) -> None:
|
|
"""Test valid timeframe values."""
|
|
valid = {"1m", "5m", "15m", "30m", "1h", "4h", "1d"}
|
|
for tf in valid:
|
|
# These should not raise
|
|
assert tf in {
|
|
"1m", "3m", "5m", "15m", "30m", "1h", "2h", "4h",
|
|
"6h", "8h", "12h", "1d", "3d", "1w", "1M"
|
|
}
|
|
|
|
def test_invalid_timeframe(self) -> None:
|
|
"""Test invalid timeframe is rejected."""
|
|
from tradefinder.core.config import Settings
|
|
|
|
invalid_timeframes = ["2m", "10m", "2d", "1y", "invalid"]
|
|
valid_set = {
|
|
"1m", "3m", "5m", "15m", "30m", "1h", "2h", "4h",
|
|
"6h", "8h", "12h", "1d", "3d", "1w", "1M"
|
|
}
|
|
for tf in invalid_timeframes:
|
|
assert tf not in valid_set
|
|
|
|
|
|
class TestTradingModes:
|
|
"""Tests for trading mode behavior."""
|
|
|
|
def test_paper_mode_no_credentials_needed(self) -> None:
|
|
"""Paper mode should not require API credentials."""
|
|
from tradefinder.core.config import TradingMode
|
|
|
|
assert TradingMode.PAPER.value == "paper"
|
|
# Paper mode is for internal simulation - no API calls
|
|
|
|
def test_testnet_mode_requires_testnet_keys(self) -> None:
|
|
"""Testnet mode requires testnet API keys."""
|
|
from tradefinder.core.config import TradingMode
|
|
|
|
assert TradingMode.TESTNET.value == "testnet"
|
|
|
|
def test_live_mode_requires_production_keys(self) -> None:
|
|
"""Live mode requires production API keys."""
|
|
from tradefinder.core.config import TradingMode
|
|
|
|
assert TradingMode.LIVE.value == "live"
|