feat(strategy): add Signal to Order converter with position sizing - Issue #36 (56 tests)

Implements comprehensive signal to order conversion framework:
- TradingSignal dataclass for BUY/SELL/HOLD signals
- SignalToOrderConverter main class
- Multiple position sizing methods (fixed, percent, risk-based, volatility)
- Configurable stop loss (fixed percent, ATR-based, trailing)
- Configurable take profit (fixed percent, R:R ratio)

Features:
- Signal strength multipliers (strong/moderate/weak)
- Confidence scaling for position sizes
- Order validation with detailed error reporting
- Lot size rounding
- Max position size limits
- Automatic stop loss and take profit orders
- Support for limit orders with configurable offset
- Batch signal conversion
- Integration with OrderRequest from broker_base

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Andrew Kaszubski 2025-12-26 22:20:18 +11:00
parent b54d6baa73
commit c423c6bdeb
3 changed files with 1754 additions and 0 deletions

View File

@ -0,0 +1,741 @@
"""Tests for Signal to Order Converter.
Issue #36: [STRAT-35] Signal to order converter
"""
from datetime import datetime
from decimal import Decimal
import pytest
from tradingagents.strategy.signal_to_order import (
# Enums
SignalType,
SignalStrength,
PositionSizingMethod,
StopLossType,
TakeProfitType,
OrderValidationError,
# Data Classes
TradingSignal,
PositionSizingConfig,
StopLossConfig,
TakeProfitConfig,
ConversionConfig,
OrderValidationResult,
ConversionResult,
# Main Class
SignalToOrderConverter,
)
from tradingagents.execution.broker_base import OrderSide, OrderType
# ============================================================================
# Enum Tests
# ============================================================================
class TestSignalType:
"""Tests for SignalType enum."""
def test_all_signal_types_defined(self):
"""Verify all signal types exist."""
assert SignalType.BUY
assert SignalType.SELL
assert SignalType.HOLD
assert SignalType.CLOSE
assert SignalType.SCALE_IN
assert SignalType.SCALE_OUT
def test_signal_values(self):
"""Verify signal type values."""
assert SignalType.BUY.value == "buy"
assert SignalType.SELL.value == "sell"
class TestSignalStrength:
"""Tests for SignalStrength enum."""
def test_all_strengths_defined(self):
"""Verify all strengths exist."""
assert SignalStrength.STRONG
assert SignalStrength.MODERATE
assert SignalStrength.WEAK
class TestPositionSizingMethod:
"""Tests for PositionSizingMethod enum."""
def test_all_methods_defined(self):
"""Verify all sizing methods exist."""
assert PositionSizingMethod.FIXED_QUANTITY
assert PositionSizingMethod.FIXED_VALUE
assert PositionSizingMethod.PERCENT_PORTFOLIO
assert PositionSizingMethod.RISK_BASED
assert PositionSizingMethod.VOLATILITY_BASED
class TestStopLossType:
"""Tests for StopLossType enum."""
def test_all_stop_types_defined(self):
"""Verify all stop loss types exist."""
assert StopLossType.FIXED_PERCENT
assert StopLossType.FIXED_AMOUNT
assert StopLossType.ATR_BASED
assert StopLossType.TRAILING
class TestTakeProfitType:
"""Tests for TakeProfitType enum."""
def test_all_tp_types_defined(self):
"""Verify all take profit types exist."""
assert TakeProfitType.FIXED_PERCENT
assert TakeProfitType.FIXED_AMOUNT
assert TakeProfitType.RISK_REWARD
# ============================================================================
# Data Class Tests
# ============================================================================
class TestTradingSignal:
"""Tests for TradingSignal dataclass."""
def test_default_creation(self):
"""Test creating signal with defaults."""
signal = TradingSignal()
assert signal.signal_id is not None
assert signal.timestamp is not None
assert signal.signal_type == SignalType.HOLD
assert signal.strength == SignalStrength.MODERATE
assert signal.confidence == Decimal("0.5")
def test_with_all_fields(self):
"""Test creating signal with all fields."""
signal = TradingSignal(
symbol="AAPL",
signal_type=SignalType.BUY,
strength=SignalStrength.STRONG,
entry_price=Decimal("150.00"),
target_price=Decimal("165.00"),
stop_price=Decimal("145.00"),
confidence=Decimal("0.85"),
reason="Bullish breakout",
)
assert signal.symbol == "AAPL"
assert signal.signal_type == SignalType.BUY
assert signal.entry_price == Decimal("150.00")
class TestPositionSizingConfig:
"""Tests for PositionSizingConfig dataclass."""
def test_default_creation(self):
"""Test creating config with defaults."""
config = PositionSizingConfig()
assert config.method == PositionSizingMethod.PERCENT_PORTFOLIO
assert config.percent_portfolio == Decimal("0.05")
assert config.max_risk_per_trade == Decimal("0.01")
def test_custom_config(self):
"""Test creating custom config."""
config = PositionSizingConfig(
method=PositionSizingMethod.RISK_BASED,
max_risk_per_trade=Decimal("0.02"),
)
assert config.method == PositionSizingMethod.RISK_BASED
class TestStopLossConfig:
"""Tests for StopLossConfig dataclass."""
def test_default_creation(self):
"""Test creating config with defaults."""
config = StopLossConfig()
assert config.type == StopLossType.FIXED_PERCENT
assert config.percent == Decimal("0.02")
assert config.enabled is True
class TestTakeProfitConfig:
"""Tests for TakeProfitConfig dataclass."""
def test_default_creation(self):
"""Test creating config with defaults."""
config = TakeProfitConfig()
assert config.type == TakeProfitType.RISK_REWARD
assert config.risk_reward_ratio == Decimal("3.0")
class TestConversionConfig:
"""Tests for ConversionConfig dataclass."""
def test_default_creation(self):
"""Test creating config with defaults."""
config = ConversionConfig()
assert config.default_order_type == OrderType.MARKET
assert config.scale_by_strength is True
class TestOrderValidationResult:
"""Tests for OrderValidationResult dataclass."""
def test_default_is_valid(self):
"""Test default is valid."""
result = OrderValidationResult()
assert result.is_valid is True
assert result.errors == []
class TestConversionResult:
"""Tests for ConversionResult dataclass."""
def test_default_creation(self):
"""Test creating result with defaults."""
result = ConversionResult()
assert result.success is True
assert result.order_request is None
# ============================================================================
# SignalToOrderConverter Tests
# ============================================================================
class TestSignalToOrderConverter:
"""Tests for SignalToOrderConverter class."""
@pytest.fixture
def converter(self):
"""Create default converter."""
return SignalToOrderConverter(
portfolio_value=Decimal("100000"),
current_prices={"AAPL": Decimal("150.00")},
)
@pytest.fixture
def buy_signal(self):
"""Create a buy signal."""
return TradingSignal(
symbol="AAPL",
signal_type=SignalType.BUY,
strength=SignalStrength.STRONG,
confidence=Decimal("0.8"),
)
@pytest.fixture
def sell_signal(self):
"""Create a sell signal."""
return TradingSignal(
symbol="AAPL",
signal_type=SignalType.SELL,
strength=SignalStrength.MODERATE,
confidence=Decimal("0.7"),
)
def test_initialization(self, converter):
"""Test converter initialization."""
assert converter.portfolio_value == Decimal("100000")
assert "AAPL" in converter.current_prices
assert converter.config is not None
def test_custom_config(self):
"""Test converter with custom config."""
config = ConversionConfig(
use_limit_orders=True,
)
converter = SignalToOrderConverter(config=config)
assert converter.config.use_limit_orders is True
def test_convert_buy_signal(self, converter, buy_signal):
"""Test converting a buy signal."""
result = converter.convert(buy_signal)
assert result.success is True
assert result.order_request is not None
assert result.order_request.side == OrderSide.BUY
assert result.order_request.symbol == "AAPL"
def test_convert_sell_signal(self, converter, sell_signal):
"""Test converting a sell signal."""
result = converter.convert(sell_signal)
assert result.success is True
assert result.order_request.side == OrderSide.SELL
def test_hold_signal_no_order(self, converter):
"""Test that HOLD signals don't generate orders."""
signal = TradingSignal(
symbol="AAPL",
signal_type=SignalType.HOLD,
)
result = converter.convert(signal)
assert result.success is False
assert "HOLD" in result.error_message
def test_position_sizing_percent_portfolio(self, converter, buy_signal):
"""Test position sizing with percent of portfolio."""
result = converter.convert(buy_signal)
# 5% of 100k = 5000, divided by 150 = ~33 shares
# With strong signal multiplier of 1.0
assert result.calculated_quantity > 0
assert result.calculated_quantity <= Decimal("100") # Reasonable size
def test_position_sizing_fixed_quantity(self, buy_signal):
"""Test fixed quantity position sizing."""
config = ConversionConfig(
position_sizing=PositionSizingConfig(
method=PositionSizingMethod.FIXED_QUANTITY,
fixed_quantity=Decimal("50"),
),
)
converter = SignalToOrderConverter(
config=config,
current_prices={"AAPL": Decimal("150.00")},
)
result = converter.convert(buy_signal)
# Should use fixed quantity (possibly scaled by strength)
assert result.calculated_quantity > 0
def test_position_sizing_fixed_value(self, buy_signal):
"""Test fixed value position sizing."""
config = ConversionConfig(
position_sizing=PositionSizingConfig(
method=PositionSizingMethod.FIXED_VALUE,
fixed_value=Decimal("7500"),
),
scale_by_strength=False,
)
converter = SignalToOrderConverter(
config=config,
current_prices={"AAPL": Decimal("150.00")},
)
result = converter.convert(buy_signal)
# 7500 / 150 = 50 shares
assert result.calculated_quantity == Decimal("50")
def test_position_sizing_risk_based(self, buy_signal):
"""Test risk-based position sizing."""
config = ConversionConfig(
position_sizing=PositionSizingConfig(
method=PositionSizingMethod.RISK_BASED,
max_risk_per_trade=Decimal("0.01"), # 1% = $1000
),
scale_by_strength=False,
)
converter = SignalToOrderConverter(
config=config,
portfolio_value=Decimal("100000"),
current_prices={"AAPL": Decimal("150.00")},
)
result = converter.convert(buy_signal)
assert result.calculated_quantity > 0
def test_strength_multiplier_strong(self, converter, buy_signal):
"""Test that strong signals get full position size."""
buy_signal.strength = SignalStrength.STRONG
result = converter.convert(buy_signal)
strong_qty = result.calculated_quantity
buy_signal.strength = SignalStrength.WEAK
result2 = converter.convert(buy_signal)
weak_qty = result2.calculated_quantity
# Strong should be >= weak (multipliers 1.0 vs 0.5)
assert strong_qty >= weak_qty
def test_stop_loss_calculated(self, converter, buy_signal):
"""Test stop loss is calculated."""
result = converter.convert(buy_signal)
assert result.calculated_stop_price is not None
# For buy, stop should be below entry
assert result.calculated_stop_price < Decimal("150.00")
def test_stop_loss_fixed_percent(self, buy_signal):
"""Test fixed percent stop loss."""
config = ConversionConfig(
stop_loss=StopLossConfig(
type=StopLossType.FIXED_PERCENT,
percent=Decimal("0.05"), # 5%
),
)
converter = SignalToOrderConverter(
config=config,
current_prices={"AAPL": Decimal("150.00")},
)
result = converter.convert(buy_signal)
# 150 * (1 - 0.05) = 142.50
assert result.calculated_stop_price == Decimal("142.50")
def test_stop_loss_atr_based(self, buy_signal):
"""Test ATR-based stop loss."""
config = ConversionConfig(
stop_loss=StopLossConfig(
type=StopLossType.ATR_BASED,
atr_multiplier=Decimal("2.0"),
),
)
converter = SignalToOrderConverter(
config=config,
current_prices={"AAPL": Decimal("150.00")},
volatility_data={"AAPL": Decimal("3.00")}, # $3 ATR
)
result = converter.convert(buy_signal)
# 150 - (3 * 2) = 144
assert result.calculated_stop_price == Decimal("144.00")
def test_take_profit_calculated(self, converter, buy_signal):
"""Test take profit is calculated."""
result = converter.convert(buy_signal)
assert result.calculated_take_profit is not None
# For buy, take profit should be above entry
assert result.calculated_take_profit > Decimal("150.00")
def test_take_profit_risk_reward(self, buy_signal):
"""Test risk:reward take profit."""
config = ConversionConfig(
stop_loss=StopLossConfig(
type=StopLossType.FIXED_PERCENT,
percent=Decimal("0.02"), # 2% stop = $3 risk
),
take_profit=TakeProfitConfig(
type=TakeProfitType.RISK_REWARD,
risk_reward_ratio=Decimal("3.0"), # 3:1
),
)
converter = SignalToOrderConverter(
config=config,
current_prices={"AAPL": Decimal("150.00")},
)
result = converter.convert(buy_signal)
# Stop at 147, risk = 3, reward = 9, target = 159
assert result.calculated_take_profit == Decimal("159.00")
def test_validation_invalid_symbol(self, converter):
"""Test validation rejects empty symbol."""
signal = TradingSignal(
symbol="",
signal_type=SignalType.BUY,
)
result = converter.convert(signal)
assert result.success is False
# Empty symbol with no price will fail on entry price first
# Or validation will catch it
assert (
"entry price" in result.error_message.lower() or
any(e == OrderValidationError.INVALID_SYMBOL for e, _ in result.validation.errors)
)
def test_validation_insufficient_price(self, converter):
"""Test validation when price not available."""
signal = TradingSignal(
symbol="UNKNOWN",
signal_type=SignalType.BUY,
)
result = converter.convert(signal)
assert result.success is False
def test_use_signal_entry_price(self, converter):
"""Test using signal's entry price."""
signal = TradingSignal(
symbol="AAPL",
signal_type=SignalType.BUY,
entry_price=Decimal("152.00"), # Different from current
)
result = converter.convert(signal)
# Stop should be based on 152, not 150
# With 2% stop: 152 * 0.98 = 148.96
assert result.calculated_stop_price == Decimal("148.96")
def test_use_signal_stop_price(self, converter):
"""Test using signal's stop price."""
signal = TradingSignal(
symbol="AAPL",
signal_type=SignalType.BUY,
stop_price=Decimal("140.00"),
)
result = converter.convert(signal)
assert result.calculated_stop_price == Decimal("140.00")
def test_use_signal_target_price(self, converter):
"""Test using signal's target price."""
signal = TradingSignal(
symbol="AAPL",
signal_type=SignalType.BUY,
target_price=Decimal("170.00"),
)
result = converter.convert(signal)
assert result.calculated_take_profit == Decimal("170.00")
def test_min_confidence(self, buy_signal):
"""Test minimum confidence threshold."""
config = ConversionConfig(
min_confidence=Decimal("0.9"),
)
converter = SignalToOrderConverter(
config=config,
current_prices={"AAPL": Decimal("150.00")},
)
buy_signal.confidence = Decimal("0.8")
result = converter.convert(buy_signal)
assert result.success is False
assert "confidence" in result.error_message.lower()
def test_order_metadata(self, converter, buy_signal):
"""Test that order includes signal metadata."""
result = converter.convert(buy_signal)
assert result.order_request.metadata["signal_id"] == buy_signal.signal_id
assert result.order_request.metadata["signal_strength"] == "strong"
def test_stop_loss_order_created(self, converter, buy_signal):
"""Test separate stop loss order is created."""
result = converter.convert(buy_signal)
assert result.stop_loss_order is not None
assert result.stop_loss_order.side == OrderSide.SELL
assert result.stop_loss_order.order_type == OrderType.STOP
def test_take_profit_order_created(self, converter, buy_signal):
"""Test separate take profit order is created."""
result = converter.convert(buy_signal)
assert result.take_profit_order is not None
assert result.take_profit_order.side == OrderSide.SELL
assert result.take_profit_order.order_type == OrderType.LIMIT
def test_limit_orders(self, buy_signal):
"""Test using limit orders instead of market."""
config = ConversionConfig(
use_limit_orders=True,
limit_order_offset=Decimal("0.01"), # 1%
)
converter = SignalToOrderConverter(
config=config,
current_prices={"AAPL": Decimal("150.00")},
)
result = converter.convert(buy_signal)
assert result.order_request.order_type == OrderType.LIMIT
# Buy limit should be above current: 150 * 1.01 = 151.50
assert result.order_request.limit_price == Decimal("151.50")
def test_convert_batch(self, converter):
"""Test batch conversion."""
signals = [
TradingSignal(symbol="AAPL", signal_type=SignalType.BUY),
TradingSignal(symbol="AAPL", signal_type=SignalType.SELL),
TradingSignal(symbol="AAPL", signal_type=SignalType.HOLD),
]
results = converter.convert_batch(signals)
assert len(results) == 3
assert results[0].success is True
assert results[1].success is True
assert results[2].success is False # HOLD
def test_update_portfolio_value(self, converter):
"""Test updating portfolio value."""
converter.update_portfolio_value(Decimal("200000"))
assert converter.portfolio_value == Decimal("200000")
def test_update_price(self, converter):
"""Test updating price."""
converter.update_price("MSFT", Decimal("300.00"))
assert converter.current_prices["MSFT"] == Decimal("300.00")
def test_update_volatility(self, converter):
"""Test updating volatility."""
converter.update_volatility("AAPL", Decimal("5.00"))
assert converter.volatility_data["AAPL"] == Decimal("5.00")
def test_scale_in_signal(self, converter):
"""Test SCALE_IN signal."""
signal = TradingSignal(
symbol="AAPL",
signal_type=SignalType.SCALE_IN,
)
result = converter.convert(signal)
assert result.success is True
assert result.order_request.side == OrderSide.BUY
def test_scale_out_signal(self, converter):
"""Test SCALE_OUT signal."""
signal = TradingSignal(
symbol="AAPL",
signal_type=SignalType.SCALE_OUT,
)
result = converter.convert(signal)
assert result.success is True
assert result.order_request.side == OrderSide.SELL
def test_close_signal(self, converter):
"""Test CLOSE signal."""
signal = TradingSignal(
symbol="AAPL",
signal_type=SignalType.CLOSE,
)
result = converter.convert(signal)
assert result.success is True
assert result.order_request.side == OrderSide.SELL
def test_sell_stop_above_entry(self, converter, sell_signal):
"""Test sell signal stop is above entry."""
result = converter.convert(sell_signal)
# For short, stop should be above entry
assert result.calculated_stop_price > Decimal("150.00")
def test_sell_take_profit_below_entry(self, converter, sell_signal):
"""Test sell signal take profit is below entry."""
result = converter.convert(sell_signal)
# For short, take profit should be below entry
assert result.calculated_take_profit < Decimal("150.00")
def test_disabled_stop_loss(self, buy_signal):
"""Test with stop loss disabled."""
config = ConversionConfig(
stop_loss=StopLossConfig(enabled=False),
)
converter = SignalToOrderConverter(
config=config,
current_prices={"AAPL": Decimal("150.00")},
)
result = converter.convert(buy_signal)
assert result.calculated_stop_price is None
assert result.stop_loss_order is None
def test_disabled_take_profit(self, buy_signal):
"""Test with take profit disabled."""
config = ConversionConfig(
take_profit=TakeProfitConfig(enabled=False),
)
converter = SignalToOrderConverter(
config=config,
current_prices={"AAPL": Decimal("150.00")},
)
result = converter.convert(buy_signal)
assert result.calculated_take_profit is None
assert result.take_profit_order is None
def test_lot_size_rounding(self, buy_signal):
"""Test rounding to lot size."""
config = ConversionConfig(
position_sizing=PositionSizingConfig(
method=PositionSizingMethod.FIXED_VALUE,
fixed_value=Decimal("7555"), # Would give 50.366...
lot_size=Decimal("10"),
),
scale_by_strength=False,
)
converter = SignalToOrderConverter(
config=config,
current_prices={"AAPL": Decimal("150.00")},
)
result = converter.convert(buy_signal)
# Should round down to 50
assert result.calculated_quantity == Decimal("50")
def test_max_position_limit(self, buy_signal):
"""Test max position size limit."""
config = ConversionConfig(
position_sizing=PositionSizingConfig(
method=PositionSizingMethod.FIXED_VALUE,
fixed_value=Decimal("50000"), # 50% of portfolio
max_position_percent=Decimal("0.10"), # But max is 10%
),
scale_by_strength=False,
)
converter = SignalToOrderConverter(
config=config,
portfolio_value=Decimal("100000"),
current_prices={"AAPL": Decimal("150.00")},
)
result = converter.convert(buy_signal)
# Max is 10k / 150 = 66 shares
assert result.calculated_quantity <= Decimal("67")
# ============================================================================
# Integration Tests
# ============================================================================
class TestSignalToOrderIntegration:
"""Integration tests for signal to order conversion."""
def test_full_workflow(self):
"""Test complete conversion workflow."""
# Setup converter with realistic config
config = ConversionConfig(
position_sizing=PositionSizingConfig(
method=PositionSizingMethod.RISK_BASED,
max_risk_per_trade=Decimal("0.01"), # 1%
),
stop_loss=StopLossConfig(
type=StopLossType.FIXED_PERCENT,
percent=Decimal("0.02"), # 2%
),
take_profit=TakeProfitConfig(
type=TakeProfitType.RISK_REWARD,
risk_reward_ratio=Decimal("3.0"),
),
)
converter = SignalToOrderConverter(
config=config,
portfolio_value=Decimal("100000"),
current_prices={"AAPL": Decimal("150.00")},
)
# Generate signal
signal = TradingSignal(
symbol="AAPL",
signal_type=SignalType.BUY,
strength=SignalStrength.STRONG,
confidence=Decimal("0.85"),
reason="Bullish breakout with volume",
)
# Convert
result = converter.convert(signal)
# Verify complete result
assert result.success is True
assert result.order_request is not None
assert result.order_request.symbol == "AAPL"
assert result.order_request.side == OrderSide.BUY
assert result.calculated_quantity > 0
assert result.calculated_stop_price == Decimal("147.00")
assert result.calculated_take_profit == Decimal("159.00")
def test_module_imports(self):
"""Test that all classes are exported from module."""
from tradingagents.strategy import (
SignalType,
SignalStrength,
PositionSizingMethod,
StopLossType,
TakeProfitType,
TradingSignal,
ConversionConfig,
SignalToOrderConverter,
)
# All imports successful
assert SignalType.BUY is not None
assert SignalToOrderConverter is not None
def test_multiple_symbols(self):
"""Test converting signals for multiple symbols."""
converter = SignalToOrderConverter(
portfolio_value=Decimal("100000"),
current_prices={
"AAPL": Decimal("150.00"),
"MSFT": Decimal("300.00"),
"GOOG": Decimal("100.00"),
},
)
signals = [
TradingSignal(symbol="AAPL", signal_type=SignalType.BUY),
TradingSignal(symbol="MSFT", signal_type=SignalType.SELL),
TradingSignal(symbol="GOOG", signal_type=SignalType.BUY),
]
results = converter.convert_batch(signals)
assert all(r.success for r in results)
assert results[0].order_request.symbol == "AAPL"
assert results[1].order_request.symbol == "MSFT"
assert results[2].order_request.symbol == "GOOG"

View File

@ -0,0 +1,96 @@
"""Strategy module for trading strategy execution.
This module provides strategy orchestration including:
- Signal to order conversion
- End-to-end strategy execution
- Position management
Issue #36: [STRAT-35] Signal to order converter
Issue #37: [STRAT-36] Strategy executor - end-to-end orchestration
Submodules:
signal_to_order: Convert signals to executable orders
Classes:
Enums:
- SignalType: Type of trading signal (buy, sell, hold)
- SignalStrength: Strength of signal (strong, moderate, weak)
- PositionSizingMethod: Position sizing method
- StopLossType: Type of stop loss
- TakeProfitType: Type of take profit
- OrderValidationError: Order validation error types
Data Classes:
- TradingSignal: A trading signal from strategy
- PositionSizingConfig: Position sizing configuration
- StopLossConfig: Stop loss configuration
- TakeProfitConfig: Take profit configuration
- ConversionConfig: Signal to order conversion config
- OrderValidationResult: Result of order validation
- ConversionResult: Result of signal to order conversion
Main Classes:
- SignalToOrderConverter: Converts signals to orders
Example:
>>> from tradingagents.strategy import (
... SignalToOrderConverter,
... TradingSignal,
... SignalType,
... ConversionConfig,
... )
>>> from decimal import Decimal
>>>
>>> converter = SignalToOrderConverter(
... portfolio_value=Decimal("100000"),
... current_prices={"AAPL": Decimal("150.00")},
... )
>>>
>>> signal = TradingSignal(
... symbol="AAPL",
... signal_type=SignalType.BUY,
... )
>>> result = converter.convert(signal)
>>> if result.success:
... print(f"Order: {result.order_request}")
"""
from .signal_to_order import (
# Enums
SignalType,
SignalStrength,
PositionSizingMethod,
StopLossType,
TakeProfitType,
OrderValidationError,
# Data Classes
TradingSignal,
PositionSizingConfig,
StopLossConfig,
TakeProfitConfig,
ConversionConfig,
OrderValidationResult,
ConversionResult,
# Main Class
SignalToOrderConverter,
)
__all__ = [
# Enums
"SignalType",
"SignalStrength",
"PositionSizingMethod",
"StopLossType",
"TakeProfitType",
"OrderValidationError",
# Data Classes
"TradingSignal",
"PositionSizingConfig",
"StopLossConfig",
"TakeProfitConfig",
"ConversionConfig",
"OrderValidationResult",
"ConversionResult",
# Main Class
"SignalToOrderConverter",
]

View File

@ -0,0 +1,917 @@
"""Signal to Order Converter.
This module converts trading signals into executable orders by:
- Translating BUY/SELL signals to OrderRequest objects
- Applying position sizing based on risk parameters
- Setting stop loss and take profit levels
- Validating orders before submission
Issue #36: [STRAT-35] Signal to order converter
Design Principles:
- Clean separation between signal generation and execution
- Risk-aware position sizing
- Configurable stop loss and take profit
- Comprehensive order validation
"""
from dataclasses import dataclass, field
from datetime import datetime
from decimal import Decimal, ROUND_DOWN
from enum import Enum
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
import uuid
from tradingagents.execution.broker_base import (
OrderRequest,
OrderSide,
OrderType,
TimeInForce,
)
# ============================================================================
# Enums
# ============================================================================
class SignalType(str, Enum):
"""Type of trading signal."""
BUY = "buy" # Long entry
SELL = "sell" # Long exit or short entry
HOLD = "hold" # No action
CLOSE = "close" # Close existing position
SCALE_IN = "scale_in" # Add to position
SCALE_OUT = "scale_out" # Reduce position
class SignalStrength(str, Enum):
"""Strength of the signal."""
STRONG = "strong" # High confidence, full size
MODERATE = "moderate" # Medium confidence, partial size
WEAK = "weak" # Low confidence, minimal size
class PositionSizingMethod(str, Enum):
"""Method for calculating position size."""
FIXED_QUANTITY = "fixed_quantity" # Fixed number of shares
FIXED_VALUE = "fixed_value" # Fixed dollar amount
PERCENT_PORTFOLIO = "percent_portfolio" # Percentage of portfolio
RISK_BASED = "risk_based" # Based on max risk per trade
VOLATILITY_BASED = "volatility_based" # Based on asset volatility
class StopLossType(str, Enum):
"""Type of stop loss."""
FIXED_PERCENT = "fixed_percent" # Fixed percentage below entry
FIXED_AMOUNT = "fixed_amount" # Fixed dollar amount below entry
ATR_BASED = "atr_based" # Multiple of ATR
VOLATILITY_BASED = "volatility_based" # Based on historical volatility
SUPPORT_LEVEL = "support_level" # At support level
TRAILING = "trailing" # Trailing stop
class TakeProfitType(str, Enum):
"""Type of take profit."""
FIXED_PERCENT = "fixed_percent" # Fixed percentage above entry
FIXED_AMOUNT = "fixed_amount" # Fixed dollar amount above entry
RISK_REWARD = "risk_reward" # Multiple of risk (R:R ratio)
RESISTANCE_LEVEL = "resistance_level" # At resistance level
TRAILING = "trailing" # Trailing take profit
class OrderValidationError(str, Enum):
"""Order validation error types."""
INVALID_SYMBOL = "invalid_symbol"
INVALID_QUANTITY = "invalid_quantity"
INVALID_PRICE = "invalid_price"
INSUFFICIENT_CAPITAL = "insufficient_capital"
POSITION_SIZE_EXCEEDED = "position_size_exceeded"
RISK_LIMIT_EXCEEDED = "risk_limit_exceeded"
INVALID_STOP_LOSS = "invalid_stop_loss"
INVALID_TAKE_PROFIT = "invalid_take_profit"
# ============================================================================
# Data Classes
# ============================================================================
@dataclass
class TradingSignal:
"""A trading signal from strategy or analyst.
Attributes:
signal_id: Unique identifier for the signal
timestamp: When the signal was generated
symbol: Trading symbol
signal_type: Type of signal (buy, sell, hold, etc.)
strength: Signal strength (strong, moderate, weak)
entry_price: Suggested entry price
target_price: Suggested target price
stop_price: Suggested stop price
confidence: Confidence level (0-1)
reason: Human-readable reason for signal
metadata: Additional signal data
"""
signal_id: str = field(default_factory=lambda: str(uuid.uuid4()))
timestamp: datetime = field(default_factory=datetime.now)
symbol: str = ""
signal_type: SignalType = SignalType.HOLD
strength: SignalStrength = SignalStrength.MODERATE
entry_price: Optional[Decimal] = None
target_price: Optional[Decimal] = None
stop_price: Optional[Decimal] = None
confidence: Decimal = Decimal("0.5")
reason: str = ""
metadata: Dict[str, Any] = field(default_factory=dict)
@dataclass
class PositionSizingConfig:
"""Configuration for position sizing.
Attributes:
method: Position sizing method
fixed_quantity: Fixed quantity for FIXED_QUANTITY method
fixed_value: Fixed value for FIXED_VALUE method
percent_portfolio: Percentage for PERCENT_PORTFOLIO method
max_risk_per_trade: Max risk per trade (as decimal, e.g., 0.01 = 1%)
max_position_percent: Max position size as % of portfolio
round_to_lot_size: Whether to round to lot sizes
lot_size: Lot size for rounding (default 1)
min_quantity: Minimum order quantity
max_quantity: Maximum order quantity
"""
method: PositionSizingMethod = PositionSizingMethod.PERCENT_PORTFOLIO
fixed_quantity: Decimal = Decimal("100")
fixed_value: Decimal = Decimal("10000")
percent_portfolio: Decimal = Decimal("0.05") # 5%
max_risk_per_trade: Decimal = Decimal("0.01") # 1%
max_position_percent: Decimal = Decimal("0.20") # 20%
round_to_lot_size: bool = True
lot_size: Decimal = Decimal("1")
min_quantity: Decimal = Decimal("1")
max_quantity: Optional[Decimal] = None
@dataclass
class StopLossConfig:
"""Configuration for stop loss.
Attributes:
type: Stop loss type
percent: Percentage for FIXED_PERCENT type
amount: Dollar amount for FIXED_AMOUNT type
atr_multiplier: ATR multiplier for ATR_BASED type
trail_percent: Trail percentage for TRAILING type
trail_amount: Trail amount for TRAILING type
enabled: Whether stop loss is enabled
"""
type: StopLossType = StopLossType.FIXED_PERCENT
percent: Decimal = Decimal("0.02") # 2%
amount: Optional[Decimal] = None
atr_multiplier: Decimal = Decimal("2.0")
trail_percent: Optional[Decimal] = None
trail_amount: Optional[Decimal] = None
enabled: bool = True
@dataclass
class TakeProfitConfig:
"""Configuration for take profit.
Attributes:
type: Take profit type
percent: Percentage for FIXED_PERCENT type
amount: Dollar amount for FIXED_AMOUNT type
risk_reward_ratio: Risk:reward ratio for RISK_REWARD type
enabled: Whether take profit is enabled
"""
type: TakeProfitType = TakeProfitType.RISK_REWARD
percent: Decimal = Decimal("0.06") # 6%
amount: Optional[Decimal] = None
risk_reward_ratio: Decimal = Decimal("3.0") # 3:1 R:R
enabled: bool = True
@dataclass
class ConversionConfig:
"""Configuration for signal to order conversion.
Attributes:
position_sizing: Position sizing configuration
stop_loss: Stop loss configuration
take_profit: Take profit configuration
default_order_type: Default order type
default_time_in_force: Default time in force
use_limit_orders: Use limit orders instead of market
limit_order_offset: Offset from signal price for limits
scale_by_strength: Scale position size by signal strength
strength_multipliers: Multipliers for each signal strength
scale_by_confidence: Scale position size by confidence
min_confidence: Minimum confidence to generate order
"""
position_sizing: PositionSizingConfig = field(
default_factory=PositionSizingConfig
)
stop_loss: StopLossConfig = field(default_factory=StopLossConfig)
take_profit: TakeProfitConfig = field(default_factory=TakeProfitConfig)
default_order_type: OrderType = OrderType.MARKET
default_time_in_force: TimeInForce = TimeInForce.DAY
use_limit_orders: bool = False
limit_order_offset: Decimal = Decimal("0.001") # 0.1%
scale_by_strength: bool = True
strength_multipliers: Dict[SignalStrength, Decimal] = field(
default_factory=lambda: {
SignalStrength.STRONG: Decimal("1.0"),
SignalStrength.MODERATE: Decimal("0.75"),
SignalStrength.WEAK: Decimal("0.5"),
}
)
scale_by_confidence: bool = False
min_confidence: Decimal = Decimal("0.0")
@dataclass
class OrderValidationResult:
"""Result of order validation.
Attributes:
is_valid: Whether the order is valid
errors: List of validation errors
warnings: List of validation warnings
adjusted_quantity: Quantity after adjustments (if any)
adjusted_stop_price: Stop price after adjustments
adjusted_take_profit: Take profit after adjustments
"""
is_valid: bool = True
errors: List[Tuple[OrderValidationError, str]] = field(default_factory=list)
warnings: List[str] = field(default_factory=list)
adjusted_quantity: Optional[Decimal] = None
adjusted_stop_price: Optional[Decimal] = None
adjusted_take_profit: Optional[Decimal] = None
@dataclass
class ConversionResult:
"""Result of signal to order conversion.
Attributes:
success: Whether conversion succeeded
order_request: The generated OrderRequest (if successful)
stop_loss_order: Stop loss order (if configured)
take_profit_order: Take profit order (if configured)
validation: Validation result
signal: Original signal
calculated_quantity: Position size calculated
calculated_stop_price: Stop loss price calculated
calculated_take_profit: Take profit price calculated
error_message: Error message (if failed)
"""
success: bool = True
order_request: Optional[OrderRequest] = None
stop_loss_order: Optional[OrderRequest] = None
take_profit_order: Optional[OrderRequest] = None
validation: OrderValidationResult = field(
default_factory=OrderValidationResult
)
signal: Optional[TradingSignal] = None
calculated_quantity: Decimal = Decimal("0")
calculated_stop_price: Optional[Decimal] = None
calculated_take_profit: Optional[Decimal] = None
error_message: str = ""
# ============================================================================
# SignalToOrderConverter Class
# ============================================================================
class SignalToOrderConverter:
"""Converts trading signals to executable orders.
This class handles the conversion of trading signals into OrderRequest
objects, applying position sizing, stop loss, and take profit logic.
Attributes:
config: Conversion configuration
portfolio_value: Current portfolio value (for sizing)
current_prices: Dict of symbol to current price
volatility_data: Dict of symbol to volatility (for ATR-based stops)
"""
def __init__(
self,
config: Optional[ConversionConfig] = None,
portfolio_value: Decimal = Decimal("100000"),
current_prices: Optional[Dict[str, Decimal]] = None,
volatility_data: Optional[Dict[str, Decimal]] = None,
):
"""Initialize the converter.
Args:
config: Conversion configuration (uses defaults if None)
portfolio_value: Current portfolio value
current_prices: Dict of symbol to current market price
volatility_data: Dict of symbol to ATR or volatility
"""
self.config = config or ConversionConfig()
self.portfolio_value = portfolio_value
self.current_prices = current_prices or {}
self.volatility_data = volatility_data or {}
def convert(self, signal: TradingSignal) -> ConversionResult:
"""Convert a trading signal to an order.
Args:
signal: The trading signal to convert
Returns:
ConversionResult with order(s) and validation info
"""
result = ConversionResult(signal=signal)
# Skip non-actionable signals
if signal.signal_type == SignalType.HOLD:
result.success = False
result.error_message = "HOLD signals do not generate orders"
return result
# Check minimum confidence
if signal.confidence < self.config.min_confidence:
result.success = False
result.error_message = (
f"Signal confidence {signal.confidence} below minimum "
f"{self.config.min_confidence}"
)
return result
# Get entry price
entry_price = self._get_entry_price(signal)
if entry_price is None:
result.success = False
result.error_message = (
f"Cannot determine entry price for {signal.symbol}"
)
return result
# Calculate position size
quantity = self._calculate_position_size(
signal=signal,
entry_price=entry_price,
)
result.calculated_quantity = quantity
# Calculate stop loss
stop_price = None
if self.config.stop_loss.enabled:
stop_price = self._calculate_stop_loss(
signal=signal,
entry_price=entry_price,
)
result.calculated_stop_price = stop_price
# Calculate take profit
take_profit_price = None
if self.config.take_profit.enabled:
take_profit_price = self._calculate_take_profit(
signal=signal,
entry_price=entry_price,
stop_price=stop_price,
)
result.calculated_take_profit = take_profit_price
# Validate the order
validation = self._validate_order(
signal=signal,
quantity=quantity,
entry_price=entry_price,
stop_price=stop_price,
take_profit_price=take_profit_price,
)
result.validation = validation
if not validation.is_valid:
result.success = False
result.error_message = "; ".join(
f"{e.value}: {msg}" for e, msg in validation.errors
)
return result
# Use adjusted values if available
final_quantity = validation.adjusted_quantity or quantity
final_stop = validation.adjusted_stop_price or stop_price
final_tp = validation.adjusted_take_profit or take_profit_price
# Determine order side
order_side = self._signal_to_side(signal.signal_type)
# Determine order type and price
order_type, limit_price = self._determine_order_type(
signal=signal,
entry_price=entry_price,
)
# Create main order
try:
main_order = OrderRequest(
symbol=signal.symbol,
side=order_side,
quantity=final_quantity,
order_type=order_type,
limit_price=limit_price,
time_in_force=self.config.default_time_in_force,
stop_loss_price=final_stop if self.config.stop_loss.enabled else None,
take_profit_price=final_tp if self.config.take_profit.enabled else None,
metadata={
"signal_id": signal.signal_id,
"signal_strength": signal.strength.value,
"signal_confidence": str(signal.confidence),
"signal_reason": signal.reason,
},
)
result.order_request = main_order
except ValueError as e:
result.success = False
result.error_message = f"Failed to create order: {str(e)}"
return result
# Create separate stop loss order if needed
if final_stop and order_side == OrderSide.BUY:
try:
result.stop_loss_order = OrderRequest(
symbol=signal.symbol,
side=OrderSide.SELL,
quantity=final_quantity,
order_type=OrderType.STOP,
stop_price=final_stop,
time_in_force=TimeInForce.GTC,
metadata={"parent_signal_id": signal.signal_id},
)
except ValueError:
# Non-fatal - main order is still valid
result.validation.warnings.append(
"Could not create separate stop loss order"
)
# Create separate take profit order if needed
if final_tp and order_side == OrderSide.BUY:
try:
result.take_profit_order = OrderRequest(
symbol=signal.symbol,
side=OrderSide.SELL,
quantity=final_quantity,
order_type=OrderType.LIMIT,
limit_price=final_tp,
time_in_force=TimeInForce.GTC,
metadata={"parent_signal_id": signal.signal_id},
)
except ValueError:
result.validation.warnings.append(
"Could not create separate take profit order"
)
result.success = True
return result
def convert_batch(
self,
signals: List[TradingSignal],
) -> List[ConversionResult]:
"""Convert multiple signals to orders.
Args:
signals: List of trading signals
Returns:
List of ConversionResult for each signal
"""
return [self.convert(signal) for signal in signals]
def _get_entry_price(self, signal: TradingSignal) -> Optional[Decimal]:
"""Get entry price for signal.
Args:
signal: The trading signal
Returns:
Entry price or None if unavailable
"""
# Use signal's entry price if available
if signal.entry_price is not None:
return signal.entry_price
# Fall back to current market price
if signal.symbol in self.current_prices:
return self.current_prices[signal.symbol]
return None
def _calculate_position_size(
self,
signal: TradingSignal,
entry_price: Decimal,
) -> Decimal:
"""Calculate position size based on configuration.
Args:
signal: The trading signal
entry_price: Entry price for the trade
Returns:
Position size in shares/contracts
"""
config = self.config.position_sizing
# Start with base size from method
if config.method == PositionSizingMethod.FIXED_QUANTITY:
base_quantity = config.fixed_quantity
elif config.method == PositionSizingMethod.FIXED_VALUE:
if entry_price > 0:
base_quantity = config.fixed_value / entry_price
else:
base_quantity = Decimal("0")
elif config.method == PositionSizingMethod.PERCENT_PORTFOLIO:
position_value = self.portfolio_value * config.percent_portfolio
if entry_price > 0:
base_quantity = position_value / entry_price
else:
base_quantity = Decimal("0")
elif config.method == PositionSizingMethod.RISK_BASED:
base_quantity = self._calculate_risk_based_size(
signal=signal,
entry_price=entry_price,
)
elif config.method == PositionSizingMethod.VOLATILITY_BASED:
base_quantity = self._calculate_volatility_based_size(
signal=signal,
entry_price=entry_price,
)
else:
base_quantity = config.fixed_quantity
# Apply strength multiplier
if self.config.scale_by_strength:
multiplier = self.config.strength_multipliers.get(
signal.strength, Decimal("1.0")
)
base_quantity *= multiplier
# Apply confidence scaling
if self.config.scale_by_confidence:
base_quantity *= signal.confidence
# Enforce max position size
max_value = self.portfolio_value * config.max_position_percent
if entry_price > 0:
max_quantity = max_value / entry_price
base_quantity = min(base_quantity, max_quantity)
# Round to lot size
if config.round_to_lot_size and config.lot_size > 0:
base_quantity = (
base_quantity / config.lot_size
).to_integral_value(rounding=ROUND_DOWN) * config.lot_size
# Enforce min/max quantity
base_quantity = max(base_quantity, config.min_quantity)
if config.max_quantity is not None:
base_quantity = min(base_quantity, config.max_quantity)
return base_quantity
def _calculate_risk_based_size(
self,
signal: TradingSignal,
entry_price: Decimal,
) -> Decimal:
"""Calculate position size based on risk per trade.
Args:
signal: The trading signal
entry_price: Entry price
Returns:
Position size in shares
"""
config = self.config.position_sizing
# Calculate dollar risk allowed
risk_dollars = self.portfolio_value * config.max_risk_per_trade
# Calculate risk per share (distance to stop)
if signal.stop_price:
risk_per_share = abs(entry_price - signal.stop_price)
else:
# Use default stop loss percentage
risk_per_share = entry_price * self.config.stop_loss.percent
if risk_per_share > 0:
return risk_dollars / risk_per_share
else:
return Decimal("0")
def _calculate_volatility_based_size(
self,
signal: TradingSignal,
entry_price: Decimal,
) -> Decimal:
"""Calculate position size based on volatility.
Args:
signal: The trading signal
entry_price: Entry price
Returns:
Position size in shares
"""
config = self.config.position_sizing
# Get ATR/volatility for symbol
volatility = self.volatility_data.get(signal.symbol, entry_price * Decimal("0.02"))
# Target volatility contribution (e.g., 1% of portfolio)
target_vol = self.portfolio_value * config.max_risk_per_trade
if volatility > 0:
return target_vol / volatility
else:
# Fall back to percent of portfolio
position_value = self.portfolio_value * config.percent_portfolio
if entry_price > 0:
return position_value / entry_price
return Decimal("0")
def _calculate_stop_loss(
self,
signal: TradingSignal,
entry_price: Decimal,
) -> Optional[Decimal]:
"""Calculate stop loss price.
Args:
signal: The trading signal
entry_price: Entry price
Returns:
Stop loss price or None
"""
config = self.config.stop_loss
# Use signal's stop price if provided
if signal.stop_price:
return signal.stop_price
# Calculate based on type
if config.type == StopLossType.FIXED_PERCENT:
if signal.signal_type in [SignalType.BUY, SignalType.SCALE_IN]:
return entry_price * (Decimal("1") - config.percent)
else:
return entry_price * (Decimal("1") + config.percent)
elif config.type == StopLossType.FIXED_AMOUNT:
if config.amount:
if signal.signal_type in [SignalType.BUY, SignalType.SCALE_IN]:
return entry_price - config.amount
else:
return entry_price + config.amount
elif config.type == StopLossType.ATR_BASED:
atr = self.volatility_data.get(
signal.symbol,
entry_price * Decimal("0.02")
)
atr_distance = atr * config.atr_multiplier
if signal.signal_type in [SignalType.BUY, SignalType.SCALE_IN]:
return entry_price - atr_distance
else:
return entry_price + atr_distance
elif config.type == StopLossType.TRAILING:
if config.trail_percent:
return entry_price * (Decimal("1") - config.trail_percent)
elif config.trail_amount:
return entry_price - config.trail_amount
return None
def _calculate_take_profit(
self,
signal: TradingSignal,
entry_price: Decimal,
stop_price: Optional[Decimal],
) -> Optional[Decimal]:
"""Calculate take profit price.
Args:
signal: The trading signal
entry_price: Entry price
stop_price: Stop loss price (for R:R calculation)
Returns:
Take profit price or None
"""
config = self.config.take_profit
# Use signal's target price if provided
if signal.target_price:
return signal.target_price
# Calculate based on type
if config.type == TakeProfitType.FIXED_PERCENT:
if signal.signal_type in [SignalType.BUY, SignalType.SCALE_IN]:
return entry_price * (Decimal("1") + config.percent)
else:
return entry_price * (Decimal("1") - config.percent)
elif config.type == TakeProfitType.FIXED_AMOUNT:
if config.amount:
if signal.signal_type in [SignalType.BUY, SignalType.SCALE_IN]:
return entry_price + config.amount
else:
return entry_price - config.amount
elif config.type == TakeProfitType.RISK_REWARD:
if stop_price:
risk = abs(entry_price - stop_price)
reward = risk * config.risk_reward_ratio
if signal.signal_type in [SignalType.BUY, SignalType.SCALE_IN]:
return entry_price + reward
else:
return entry_price - reward
return None
def _validate_order(
self,
signal: TradingSignal,
quantity: Decimal,
entry_price: Decimal,
stop_price: Optional[Decimal],
take_profit_price: Optional[Decimal],
) -> OrderValidationResult:
"""Validate the generated order.
Args:
signal: Original signal
quantity: Calculated quantity
entry_price: Entry price
stop_price: Stop loss price
take_profit_price: Take profit price
Returns:
OrderValidationResult
"""
result = OrderValidationResult()
# Validate symbol
if not signal.symbol:
result.is_valid = False
result.errors.append((
OrderValidationError.INVALID_SYMBOL,
"Symbol is required"
))
# Validate quantity
if quantity <= 0:
result.is_valid = False
result.errors.append((
OrderValidationError.INVALID_QUANTITY,
f"Quantity must be positive, got {quantity}"
))
# Validate entry price
if entry_price <= 0:
result.is_valid = False
result.errors.append((
OrderValidationError.INVALID_PRICE,
f"Entry price must be positive, got {entry_price}"
))
# Validate position value vs portfolio
position_value = quantity * entry_price
max_position = self.portfolio_value * self.config.position_sizing.max_position_percent
if position_value > max_position:
result.warnings.append(
f"Position value {position_value} exceeds max {max_position}, "
f"adjusting quantity"
)
result.adjusted_quantity = (max_position / entry_price).to_integral_value(
rounding=ROUND_DOWN
)
# Validate stop loss
if stop_price is not None:
if signal.signal_type in [SignalType.BUY, SignalType.SCALE_IN]:
if stop_price >= entry_price:
result.is_valid = False
result.errors.append((
OrderValidationError.INVALID_STOP_LOSS,
f"Stop loss {stop_price} must be below entry {entry_price} for BUY"
))
else:
if stop_price <= entry_price:
result.is_valid = False
result.errors.append((
OrderValidationError.INVALID_STOP_LOSS,
f"Stop loss {stop_price} must be above entry {entry_price} for SELL"
))
# Validate take profit
if take_profit_price is not None:
if signal.signal_type in [SignalType.BUY, SignalType.SCALE_IN]:
if take_profit_price <= entry_price:
result.is_valid = False
result.errors.append((
OrderValidationError.INVALID_TAKE_PROFIT,
f"Take profit {take_profit_price} must be above entry {entry_price} for BUY"
))
else:
if take_profit_price >= entry_price:
result.is_valid = False
result.errors.append((
OrderValidationError.INVALID_TAKE_PROFIT,
f"Take profit {take_profit_price} must be below entry {entry_price} for SELL"
))
# Check risk limit
if stop_price is not None and result.is_valid:
risk_per_share = abs(entry_price - stop_price)
final_qty = result.adjusted_quantity or quantity
total_risk = risk_per_share * final_qty
max_risk = self.portfolio_value * self.config.position_sizing.max_risk_per_trade
if total_risk > max_risk * Decimal("2"): # Allow some buffer
result.warnings.append(
f"Total risk {total_risk} high relative to max {max_risk}"
)
return result
def _signal_to_side(self, signal_type: SignalType) -> OrderSide:
"""Convert signal type to order side.
Args:
signal_type: The signal type
Returns:
OrderSide (BUY or SELL)
"""
if signal_type in [SignalType.BUY, SignalType.SCALE_IN]:
return OrderSide.BUY
else:
return OrderSide.SELL
def _determine_order_type(
self,
signal: TradingSignal,
entry_price: Decimal,
) -> Tuple[OrderType, Optional[Decimal]]:
"""Determine order type and limit price.
Args:
signal: The trading signal
entry_price: Entry price
Returns:
Tuple of (OrderType, limit_price or None)
"""
if self.config.use_limit_orders:
# Calculate limit price with offset
offset = entry_price * self.config.limit_order_offset
if signal.signal_type in [SignalType.BUY, SignalType.SCALE_IN]:
limit_price = entry_price + offset
else:
limit_price = entry_price - offset
return OrderType.LIMIT, limit_price
else:
return self.config.default_order_type, None
def update_portfolio_value(self, value: Decimal):
"""Update the portfolio value.
Args:
value: New portfolio value
"""
self.portfolio_value = value
def update_price(self, symbol: str, price: Decimal):
"""Update the current price for a symbol.
Args:
symbol: Trading symbol
price: Current market price
"""
self.current_prices[symbol] = price
def update_volatility(self, symbol: str, volatility: Decimal):
"""Update the volatility/ATR for a symbol.
Args:
symbol: Trading symbol
volatility: ATR or volatility value
"""
self.volatility_data[symbol] = volatility