phase 14-15: docker build validation and helm deployment

This commit is contained in:
Celes Renata
2026-04-11 11:59:45 -07:00
parent 7394d241c9
commit ce10afa034
179 changed files with 32559 additions and 576 deletions
+261
View File
@@ -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