Add core infrastructure: config, Binance adapter, docs, and auto-setup
- 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
This commit is contained in:
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
154
tests/test_config.py
Normal file
154
tests/test_config.py
Normal file
@@ -0,0 +1,154 @@
|
||||
"""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"
|
||||
Reference in New Issue
Block a user