TradingAgents/tradingagents/strategy/signal_to_order.py

918 lines
32 KiB
Python

"""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