phase 14-15: docker build validation and helm deployment
This commit is contained in:
@@ -0,0 +1,261 @@
|
||||
"""Tests for the broker service - sandbox integration wiring.
|
||||
|
||||
Validates job parsing, risk evaluation integration, order building,
|
||||
and the overall process_order_job flow using a mock Alpaca adapter.
|
||||
"""
|
||||
import pytest
|
||||
|
||||
from services.adapters.broker_adapter import (
|
||||
AlpacaBrokerAdapter,
|
||||
OrderRequest,
|
||||
OrderResponse,
|
||||
OrderSide,
|
||||
OrderStatus,
|
||||
OrderType,
|
||||
TradingMode,
|
||||
)
|
||||
from services.adapters.broker_service import (
|
||||
build_order_request,
|
||||
build_proposed_order,
|
||||
generate_idempotency_key,
|
||||
)
|
||||
from services.risk.engine import (
|
||||
AccountRiskState,
|
||||
PortfolioRiskConfig,
|
||||
ProposedOrder,
|
||||
TradingMode as RiskTradingMode,
|
||||
evaluate_order,
|
||||
)
|
||||
from services.shared.redis_keys import QUEUE_BROKER
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# build_order_request tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestBuildOrderRequest:
|
||||
def test_basic_buy_market(self):
|
||||
job = {
|
||||
"ticker": "AAPL",
|
||||
"side": "buy",
|
||||
"quantity": 10,
|
||||
"order_type": "market",
|
||||
"idempotency_key": "key-1",
|
||||
}
|
||||
req = build_order_request(job)
|
||||
assert req.ticker == "AAPL"
|
||||
assert req.side == OrderSide.BUY
|
||||
assert req.quantity == 10
|
||||
assert req.order_type == OrderType.MARKET
|
||||
assert req.idempotency_key == "key-1"
|
||||
|
||||
def test_sell_limit_order(self):
|
||||
job = {
|
||||
"ticker": "MSFT",
|
||||
"side": "sell",
|
||||
"quantity": 5,
|
||||
"order_type": "limit",
|
||||
"limit_price": 400.0,
|
||||
}
|
||||
req = build_order_request(job)
|
||||
assert req.side == OrderSide.SELL
|
||||
assert req.order_type == OrderType.LIMIT
|
||||
assert req.limit_price == 400.0
|
||||
|
||||
def test_stop_order(self):
|
||||
job = {
|
||||
"ticker": "TSLA",
|
||||
"side": "sell",
|
||||
"quantity": 3,
|
||||
"order_type": "stop",
|
||||
"stop_price": 200.0,
|
||||
}
|
||||
req = build_order_request(job)
|
||||
assert req.order_type == OrderType.STOP
|
||||
assert req.stop_price == 200.0
|
||||
|
||||
def test_defaults(self):
|
||||
job = {"ticker": "GOOG"}
|
||||
req = build_order_request(job)
|
||||
assert req.side == OrderSide.BUY
|
||||
assert req.quantity == 0
|
||||
assert req.order_type == OrderType.MARKET
|
||||
assert req.time_in_force == "day"
|
||||
assert req.idempotency_key # deterministic from job content
|
||||
|
||||
def test_deterministic_key_without_explicit(self):
|
||||
"""Without an explicit key, the same job produces the same key."""
|
||||
job = {"ticker": "AAPL", "side": "buy", "quantity": 10}
|
||||
req1 = build_order_request(job)
|
||||
req2 = build_order_request(job)
|
||||
assert req1.idempotency_key == req2.idempotency_key
|
||||
|
||||
def test_custom_time_in_force(self):
|
||||
job = {"ticker": "AAPL", "time_in_force": "gtc"}
|
||||
req = build_order_request(job)
|
||||
assert req.time_in_force == "gtc"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# build_proposed_order tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestBuildProposedOrder:
|
||||
def test_basic_proposed_order(self):
|
||||
job = {
|
||||
"ticker": "AAPL",
|
||||
"side": "buy",
|
||||
"quantity": 10,
|
||||
"estimated_value": 1500.0,
|
||||
"confidence": 0.85,
|
||||
"sector": "technology",
|
||||
"recommendation_id": "rec-123",
|
||||
}
|
||||
proposed = build_proposed_order(job)
|
||||
assert proposed.ticker == "AAPL"
|
||||
assert proposed.action == "buy"
|
||||
assert proposed.quantity == 10
|
||||
assert proposed.estimated_value == 1500.0
|
||||
assert proposed.confidence == 0.85
|
||||
assert proposed.sector == "technology"
|
||||
assert proposed.recommendation_id == "rec-123"
|
||||
|
||||
def test_defaults(self):
|
||||
job = {"ticker": "GOOG"}
|
||||
proposed = build_proposed_order(job)
|
||||
assert proposed.action == "buy"
|
||||
assert proposed.quantity == 0
|
||||
assert proposed.estimated_value == 0
|
||||
assert proposed.sector == ""
|
||||
assert proposed.recommendation_id is None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Risk evaluation integration with broker service flow
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestRiskEvaluationIntegration:
|
||||
"""Verify that risk evaluation correctly gates order submission."""
|
||||
|
||||
def test_order_passes_risk_in_paper_mode(self):
|
||||
config = PortfolioRiskConfig(trading_mode=RiskTradingMode.PAPER)
|
||||
state = AccountRiskState(
|
||||
portfolio_value=100_000.0,
|
||||
cash=50_000.0,
|
||||
buying_power=50_000.0,
|
||||
)
|
||||
proposed = ProposedOrder(
|
||||
ticker="AAPL",
|
||||
action="buy",
|
||||
quantity=10,
|
||||
estimated_value=1500.0,
|
||||
sector="technology",
|
||||
)
|
||||
result = evaluate_order(proposed, config, state)
|
||||
assert result.eligible
|
||||
assert result.allowed_mode == RiskTradingMode.PAPER
|
||||
|
||||
def test_order_blocked_when_trading_disabled(self):
|
||||
config = PortfolioRiskConfig(trading_mode=RiskTradingMode.DISABLED)
|
||||
proposed = ProposedOrder(ticker="AAPL", quantity=10, estimated_value=1500.0)
|
||||
result = evaluate_order(proposed, config)
|
||||
assert not result.eligible
|
||||
assert "disabled" in result.rejection_reasons[0].lower()
|
||||
|
||||
def test_order_blocked_by_position_size(self):
|
||||
config = PortfolioRiskConfig(trading_mode=RiskTradingMode.PAPER)
|
||||
config.position_limits.max_position_value = 1000.0
|
||||
state = AccountRiskState(portfolio_value=100_000.0)
|
||||
proposed = ProposedOrder(
|
||||
ticker="AAPL",
|
||||
quantity=100,
|
||||
estimated_value=15_000.0,
|
||||
)
|
||||
result = evaluate_order(proposed, config, state)
|
||||
assert not result.eligible
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Alpaca adapter sandbox mode verification
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestAlpacaSandboxMode:
|
||||
def test_paper_mode_uses_sandbox_url(self):
|
||||
adapter = AlpacaBrokerAdapter(
|
||||
api_key="test-key",
|
||||
api_secret="test-secret",
|
||||
mode=TradingMode.PAPER,
|
||||
)
|
||||
assert adapter.mode == TradingMode.PAPER
|
||||
assert "paper" in adapter.base_url
|
||||
|
||||
def test_custom_sandbox_url(self):
|
||||
adapter = AlpacaBrokerAdapter(
|
||||
api_key="test-key",
|
||||
api_secret="test-secret",
|
||||
mode=TradingMode.PAPER,
|
||||
base_url="https://paper-api.alpaca.markets",
|
||||
)
|
||||
assert adapter.base_url == "https://paper-api.alpaca.markets"
|
||||
|
||||
def test_headers_set_correctly(self):
|
||||
adapter = AlpacaBrokerAdapter(
|
||||
api_key="pk-test",
|
||||
api_secret="sk-test",
|
||||
)
|
||||
headers = adapter._headers()
|
||||
assert headers["APCA-API-KEY-ID"] == "pk-test"
|
||||
assert headers["APCA-API-SECRET-KEY"] == "sk-test"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Queue name constant
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestQueueConstant:
|
||||
def test_broker_queue_name(self):
|
||||
assert QUEUE_BROKER == "broker_orders"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Idempotency key generation tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestGenerateIdempotencyKey:
|
||||
def test_explicit_key_passthrough(self):
|
||||
job = {"ticker": "AAPL", "idempotency_key": "my-explicit-key"}
|
||||
assert generate_idempotency_key(job) == "my-explicit-key"
|
||||
|
||||
def test_deterministic_without_explicit_key(self):
|
||||
job = {"ticker": "AAPL", "side": "buy", "quantity": 10, "order_type": "market"}
|
||||
key1 = generate_idempotency_key(job)
|
||||
key2 = generate_idempotency_key(job)
|
||||
assert key1 == key2
|
||||
assert len(key1) == 40 # sha256 truncated to 40 chars
|
||||
|
||||
def test_different_jobs_produce_different_keys(self):
|
||||
job_a = {"ticker": "AAPL", "side": "buy", "quantity": 10}
|
||||
job_b = {"ticker": "AAPL", "side": "sell", "quantity": 10}
|
||||
assert generate_idempotency_key(job_a) != generate_idempotency_key(job_b)
|
||||
|
||||
def test_quantity_difference_changes_key(self):
|
||||
job_a = {"ticker": "AAPL", "side": "buy", "quantity": 10}
|
||||
job_b = {"ticker": "AAPL", "side": "buy", "quantity": 20}
|
||||
assert generate_idempotency_key(job_a) != generate_idempotency_key(job_b)
|
||||
|
||||
def test_recommendation_id_included(self):
|
||||
job_a = {"ticker": "AAPL", "recommendation_id": "rec-1"}
|
||||
job_b = {"ticker": "AAPL", "recommendation_id": "rec-2"}
|
||||
assert generate_idempotency_key(job_a) != generate_idempotency_key(job_b)
|
||||
|
||||
def test_minimal_job_still_produces_key(self):
|
||||
job = {"ticker": "AAPL"}
|
||||
key = generate_idempotency_key(job)
|
||||
assert key
|
||||
assert len(key) == 40
|
||||
Reference in New Issue
Block a user