Files
biggfish/src/core/portfolio.py
T
sami7777 82c68fa8b0 Scalping strategy overhaul: bidirectional trading, oil/JPY focus
- Switch to oil stocks (USO, XLE, OXY, CVX, XOM, SLB, HAL, DVN, MPC, VLO)
- Add JPY/USD forex pairs for Japan targeting
- 7-action RL space: long, short, close (was 5 long-only actions)
- Bollinger Band mean-reversion scalp entries both directions
- 5-minute candles with 60-second cycles for scalping
- 35 features (added VWAP, fast RSI, fast ROC for scalping)
- Short position support in backtest, executor, and RL environment
- GA tuned for scalping: tighter SL/TP, shorter hold times

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 16:45:19 -07:00

236 lines
7.2 KiB
Python

"""
Portfolio Management and Calculations
Handles portfolio analysis, allocation calculations, and rebalancing math
"""
from typing import Dict, List, Tuple
from decimal import Decimal, ROUND_DOWN
class Portfolio:
"""Portfolio management and calculation utilities"""
def __init__(self, adapter):
"""
Initialize portfolio manager
Args:
adapter: Exchange or broker adapter instance
"""
self.adapter = adapter
def get_current_allocation(self) -> Dict[str, float]:
"""
Get current portfolio allocation percentages
Returns:
Dictionary mapping symbols to percentages
"""
return self.adapter.get_current_allocation()
def calculate_rebalance_trades(
self,
target_allocation: Dict[str, float],
threshold: float = 0,
min_trade_value: Decimal = Decimal("10")
) -> List[Dict]:
"""
Calculate trades needed to rebalance portfolio to target allocation
Args:
target_allocation: Target allocation percentages (e.g., {"BTC": 40, "ETH": 30, "USDT": 30})
threshold: Minimum drift percentage before rebalancing (default: 0)
min_trade_value: Minimum trade value to execute
Returns:
List of trade dictionaries with symbol, action, and amount
"""
current_allocation = self.get_current_allocation()
total_value = self.adapter.get_portfolio_value()
if total_value == 0:
print("Portfolio value is zero, cannot rebalance")
return []
# Normalize target allocation to 100%
total_target = sum(target_allocation.values())
if total_target == 0:
print("Target allocation sums to zero")
return []
normalized_target = {
symbol: (pct / total_target) * 100
for symbol, pct in target_allocation.items()
}
# Calculate drifts
drifts = {}
for symbol in set(list(current_allocation.keys()) + list(normalized_target.keys())):
current = current_allocation.get(symbol, 0)
target = normalized_target.get(symbol, 0)
drift = target - current
drifts[symbol] = drift
# Check if rebalancing is needed
max_drift = max(abs(d) for d in drifts.values())
if max_drift < threshold:
print(f"Maximum drift {max_drift:.2f}% is below threshold {threshold}%")
return []
# Calculate trade amounts
trades = []
for symbol, drift in drifts.items():
if abs(drift) < 0.1: # Ignore tiny drifts
continue
# Calculate trade value
trade_value = (Decimal(str(drift)) / 100) * total_value
if abs(trade_value) < min_trade_value:
continue
# Determine action
if drift > 0:
action = "buy"
else:
action = "sell"
trade_value = abs(trade_value)
trades.append({
'symbol': symbol,
'action': action,
'value': trade_value,
'drift': drift
})
return trades
def calculate_trade_amounts(
self,
trades: List[Dict],
base_currency: str = 'USDT'
) -> List[Dict]:
"""
Convert trade values to actual amounts based on current prices
Args:
trades: List of trades from calculate_rebalance_trades
base_currency: Base currency for valuation
Returns:
Updated trade list with amounts
"""
updated_trades = []
for trade in trades:
symbol = trade['symbol']
value = trade['value']
# If trading the base currency, amount = value
if symbol == base_currency:
trade['amount'] = value
updated_trades.append(trade)
continue
# Get current price
pair = f"{symbol}/{base_currency}"
try:
price = self.adapter.get_price(pair)
if price > 0:
amount = (value / price).quantize(Decimal('0.00000001'), rounding=ROUND_DOWN)
trade['amount'] = amount
trade['price'] = price
updated_trades.append(trade)
except Exception as e:
print(f"Error calculating amount for {symbol}: {e}")
continue
return updated_trades
def validate_trade(self, trade: Dict, safety_config: Dict) -> Tuple[bool, str]:
"""
Validate a trade against safety parameters
Args:
trade: Trade dictionary
safety_config: Safety configuration
Returns:
Tuple of (is_valid, reason)
"""
min_trade_value = Decimal(str(safety_config.get('minTradeValue', 10)))
# Check minimum trade value
if trade.get('value', 0) < min_trade_value:
return False, f"Trade value {trade['value']} below minimum {min_trade_value}"
# Add more validations as needed
return True, "Valid"
def execute_rebalance(
self,
target_allocation: Dict[str, float],
threshold: float = 5,
dry_run: bool = True,
safety_config: Dict = None
) -> Dict:
"""
Execute full rebalancing operation
Args:
target_allocation: Target allocation percentages
threshold: Drift threshold for rebalancing
dry_run: If True, don't execute trades
safety_config: Safety parameters
Returns:
Results dictionary with trades and status
"""
if safety_config is None:
safety_config = {'minTradeValue': 10}
# Calculate trades
trades = self.calculate_rebalance_trades(target_allocation, threshold)
if not trades:
return {'status': 'no_rebalance_needed', 'trades': []}
# Calculate amounts
trades_with_amounts = self.calculate_trade_amounts(trades)
# Validate and execute
results = {
'status': 'completed' if not dry_run else 'dry_run',
'trades': [],
'errors': []
}
for trade in trades_with_amounts:
# Validate
is_valid, reason = self.validate_trade(trade, safety_config)
if not is_valid:
results['errors'].append({
'trade': trade,
'reason': reason
})
continue
# Execute if not dry run
if not dry_run:
try:
order = self.adapter.create_market_order(
symbol=trade['symbol'],
side=trade['action'],
amount=trade['amount']
)
trade['order'] = order
results['trades'].append(trade)
except Exception as e:
results['errors'].append({
'trade': trade,
'error': str(e)
})
else:
results['trades'].append(trade)
return results