| Overall Statistics |
|
Total Orders 414965 Average Win 0.01% Average Loss 0.00% Compounding Annual Return -26.107% Drawdown 77.400% Expectancy -0.204 Start Equity 10000000 End Equity 2258514.51 Net Profit -77.415% Sharpe Ratio -2.68 Sortino Ratio -4.052 Probabilistic Sharpe Ratio 0.000% Loss Rate 83% Win Rate 17% Profit-Loss Ratio 3.58 Alpha -0.211 Beta -0.019 Annual Standard Deviation 0.079 Annual Variance 0.006 Information Ratio -1.569 Tracking Error 0.195 Treynor Ratio 11.287 Total Fees $1620969.78 Estimated Strategy Capacity $24000000.00 Lowest Capacity Asset PH R735QTJ8XC9X Portfolio Turnover 151.65% Drawdown Recovery 0 |
# from AlgorithmImports import *
# import numpy as np
# import pandas as pd
# from datetime import timedelta
# from collections import deque
# class KMRFGapTradingSimplified(QCAlgorithm):
# """
# Production-ready version combining KMRF Regime Detection with Gap Trading
# Simplified for direct QuantConnect deployment
# REVERSED: Longs become shorts and shorts become longs
# """
# def Initialize(self):
# self.SetStartDate(2024, 1, 1)
# self.SetEndDate(2024, 12, 1)
# self.SetCash(10000000)
# # === REGIME PARAMETERS ===
# self.kama_period = 30
# self.volatility_lookback = 20
# # === GAP TRADING PARAMETERS ===
# self.gap_threshold = 0.03 # 3% gap
# self.holding_period = timedelta(days=8)
# self.atr_period = 14
# # === PORTFOLIO PARAMETERS ===
# self.max_gross_exposure = 1.5 # 150% max
# # === UNIVERSE ===
# self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol
# self.AddUniverseSelection(ETFConstituentsUniverseSelectionModel(self.spy))
# # Add VIX for volatility regime
# self.vix = self.AddData(CBOE, "VIX", Resolution.Daily).Symbol
# # === DATA STORAGE ===
# self.current_regime = "neutral"
# self.regime_confidence = 0.5
# self.kama_value = None
# self.data = {}
# self.atr_indicators = {}
# self.long_positions = {}
# self.short_positions = {}
# # Schedule daily execution
# self.Schedule.On(
# self.DateRules.EveryDay(self.spy),
# self.TimeRules.AfterMarketOpen(self.spy, 30),
# self.ExecuteStrategy
# )
# self.SetWarmUp(60, Resolution.Daily)
# def ExecuteStrategy(self):
# """Main execution combining regime detection and gap trading"""
# # Step 1: Detect Market Regime
# self.UpdateRegime()
# # Step 2: Scan for Gap Signals
# self.ScanForGaps()
# # Step 3: Execute Trades with Regime Bias
# self.ExecuteTradesWithRegimeBias()
# # Step 4: Manage Exits
# self.ManageExits()
# def UpdateRegime(self):
# """Detect market regime using KAMA and volatility"""
# # Get SPY history
# history = self.History(self.spy, self.kama_period + 20, Resolution.Daily)
# if history.empty:
# return
# closes = history['close'].values
# # Calculate KAMA (trend detection)
# kama = self.CalculateKAMA(closes)
# # Determine trend
# if kama is not None:
# if closes[-1] > kama * 1.01: # 1% above KAMA
# trend = "bullish"
# elif closes[-1] < kama * 0.99: # 1% below KAMA
# trend = "bearish"
# else:
# trend = "neutral"
# else:
# trend = "neutral"
# # Calculate volatility regime
# returns = np.diff(closes) / closes[:-1]
# recent_vol = np.std(returns[-10:]) if len(returns) >= 10 else 0
# historical_vol = np.std(returns) if len(returns) > 20 else recent_vol
# if recent_vol > historical_vol * 1.5:
# vol_regime = "high"
# else:
# vol_regime = "low"
# # Combine regimes with CONTRARIAN interpretation
# if trend == "bullish" and vol_regime == "low":
# # Overbought -> Bearish signal
# self.current_regime = "bearish"
# self.regime_confidence = 0.7
# elif trend == "bearish" and vol_regime == "high":
# # Oversold -> Bullish signal
# self.current_regime = "bullish"
# self.regime_confidence = 0.7
# else:
# self.current_regime = "neutral"
# self.regime_confidence = 0.5
# self.Debug(f"Regime: {self.current_regime} (Trend: {trend}, Vol: {vol_regime})")
# def CalculateKAMA(self, prices):
# """Kaufman's Adaptive Moving Average"""
# if len(prices) < self.kama_period:
# return None
# # Efficiency Ratio
# direction = abs(prices[-1] - prices[-self.kama_period])
# volatility = sum(abs(prices[i] - prices[i-1]) for i in range(-self.kama_period+1, 0))
# if volatility == 0:
# er = 1
# else:
# er = direction / volatility
# # Smoothing constant
# fastest = 2 / (2 + 1)
# slowest = 2 / (30 + 1)
# sc = (er * (fastest - slowest) + slowest) ** 2
# # KAMA calculation
# if self.kama_value is None:
# self.kama_value = prices[-1]
# else:
# self.kama_value = self.kama_value + sc * (prices[-1] - self.kama_value)
# return self.kama_value
# def ScanForGaps(self):
# """Identify gap opportunities with regime adjustment - REVERSED"""
# for kvp in self.CurrentSlice.Bars:
# symbol = kvp.Key
# bar = kvp.Value
# # Skip SPY and other indices
# if symbol == self.spy:
# continue
# # Initialize data storage
# if symbol not in self.data:
# self.data[symbol] = {'previous_close': None}
# self.atr_indicators[symbol] = self.ATR(symbol, self.atr_period)
# # Check for gaps
# if self.data[symbol]['previous_close'] is not None:
# prev_close = self.data[symbol]['previous_close']
# gap = (bar.Open - prev_close) / prev_close
# # Adjust thresholds based on regime
# if self.current_regime == "bullish":
# long_threshold = self.gap_threshold * 0.8
# short_threshold = self.gap_threshold * 1.2
# elif self.current_regime == "bearish":
# long_threshold = self.gap_threshold * 1.2
# short_threshold = self.gap_threshold * 0.8
# else:
# long_threshold = self.gap_threshold
# short_threshold = self.gap_threshold
# # REVERSED: Gap up -> SHORT signal
# if gap > long_threshold and symbol not in self.short_positions:
# self.short_positions[symbol] = {
# 'entry_time': self.Time,
# 'gap_size': gap,
# 'entry_price': bar.Open
# }
# if symbol in self.long_positions:
# del self.long_positions[symbol]
# # REVERSED: Gap down -> LONG signal
# elif gap < -short_threshold and symbol not in self.long_positions:
# self.long_positions[symbol] = {
# 'entry_time': self.Time,
# 'gap_size': gap,
# 'entry_price': bar.Open
# }
# if symbol in self.short_positions:
# del self.short_positions[symbol]
# # Update previous close
# self.data[symbol]['previous_close'] = bar.Close
# def ExecuteTradesWithRegimeBias(self):
# """Execute trades with directional portfolio bias - REVERSED"""
# num_longs = len(self.long_positions)
# num_shorts = len(self.short_positions)
# total_positions = num_longs + num_shorts
# if total_positions == 0:
# return
# # REVERSED: Calculate regime-based allocation
# if self.current_regime == "bullish":
# # Bullish: 30% long, 70% short (reversed)
# long_allocation = 0.3 * self.max_gross_exposure
# short_allocation = 0.7 * self.max_gross_exposure
# elif self.current_regime == "bearish":
# # Bearish: 70% long, 30% short (reversed)
# long_allocation = 0.7 * self.max_gross_exposure
# short_allocation = 0.3 * self.max_gross_exposure
# else:
# # Neutral: 50/50
# long_allocation = 0.5 * self.max_gross_exposure
# short_allocation = 0.5 * self.max_gross_exposure
# # Execute long positions
# if num_longs > 0:
# weight_per_long = min(long_allocation / num_longs, 0.10)
# for symbol in self.long_positions:
# self.SetHoldings(symbol, weight_per_long)
# self.SetStopLoss(symbol, True)
# # Execute short positions
# if num_shorts > 0:
# weight_per_short = min(short_allocation / num_shorts, 0.10)
# for symbol in self.short_positions:
# self.SetHoldings(symbol, -weight_per_short)
# self.SetStopLoss(symbol, False)
# def SetStopLoss(self, symbol, is_long):
# """Set ATR-based stop loss"""
# if symbol in self.atr_indicators and self.atr_indicators[symbol].IsReady:
# atr = self.atr_indicators[symbol].Current.Value
# current_price = self.Securities[symbol].Price
# if is_long:
# stop_price = current_price - 2 * atr
# self.StopMarketOrder(symbol, -self.Portfolio[symbol].Quantity, stop_price)
# else:
# stop_price = current_price + 2 * atr
# self.StopMarketOrder(symbol, -self.Portfolio[symbol].Quantity, stop_price)
# def ManageExits(self):
# """Exit positions based on holding period or regime change"""
# current_time = self.Time
# # Check long positions
# for symbol in list(self.long_positions.keys()):
# position_info = self.long_positions[symbol]
# exit = False
# if current_time - position_info['entry_time'] >= self.holding_period:
# exit = True
# if self.current_regime == "bearish" and self.regime_confidence > 0.65:
# exit = True
# if exit:
# self.Liquidate(symbol)
# del self.long_positions[symbol]
# self.Debug(f"Exited long: {symbol}")
# # Check short positions
# for symbol in list(self.short_positions.keys()):
# position_info = self.short_positions[symbol]
# exit = False
# if current_time - position_info['entry_time'] >= self.holding_period:
# exit = True
# if self.current_regime == "bullish" and self.regime_confidence > 0.65:
# exit = True
# if exit:
# self.Liquidate(symbol)
# del self.short_positions[symbol]
# self.Debug(f"Exited short: {symbol}")
# def OnEndOfDay(self, symbol):
# """Log daily summary"""
# if symbol == self.spy:
# self.Debug(f"""
# Date: {self.Time.date()}
# Regime: {self.current_regime} ({self.regime_confidence:.1%})
# Longs: {len(self.long_positions)}
# Shorts: {len(self.short_positions)}
# """)
##########################################################################################################
from AlgorithmImports import *
import numpy as np
import pandas as pd
from datetime import timedelta
from collections import deque
from scipy import stats
from sklearn.mixture import GaussianMixture
class HMMGapTradingAlgorithm(QCAlgorithm):
"""
Gap Trading with Hidden Markov Model Regime Detection
Uses unsupervised learning to identify market regimes
"""
def Initialize(self):
self.SetStartDate(2020, 1, 1)
self.SetEndDate(2024, 12, 1)
self.SetCash(10000000)
# === HMM PARAMETERS ===
self.n_regimes = 3 # Bull, Bear, Neutral
self.lookback_days = 100 # Days of history for HMM
self.hmm_features_window = 20 # Window for feature calculation
self.regime_confidence_threshold = 0.6
# === GAP TRADING PARAMETERS ===
self.gap_threshold = 0.03
self.holding_period = timedelta(days=8)
self.atr_period = 14
self.max_gross_exposure = 1.5
# === UNIVERSE ===
self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol
self.AddUniverseSelection(ETFConstituentsUniverseSelectionModel(self.spy))
# === HMM MODEL STORAGE ===
self.hmm_model = None
self.current_regime = 1 # 0=Bear, 1=Neutral, 2=Bull
self.regime_probabilities = np.array([0.33, 0.34, 0.33])
self.regime_history = deque(maxlen=252)
# === TRADING DATA ===
self.data = {}
self.atr_indicators = {}
self.long_positions = {}
self.short_positions = {}
# === REGIME CHARACTERISTICS ===
self.regime_stats = {
0: {'name': 'bear', 'mean_return': -0.001, 'volatility': 0.02},
1: {'name': 'neutral', 'mean_return': 0.0003, 'volatility': 0.01},
2: {'name': 'bull', 'mean_return': 0.001, 'volatility': 0.015}
}
# Schedule functions
self.Schedule.On(
self.DateRules.EveryDay(self.spy),
self.TimeRules.AfterMarketOpen(self.spy, 30),
self.ExecuteStrategy
)
# Weekly HMM model update
self.Schedule.On(
self.DateRules.WeekStart(self.spy),
self.TimeRules.AfterMarketOpen(self.spy, 60),
self.TrainHMM
)
self.SetWarmUp(self.lookback_days, Resolution.Daily)
self.Debug("HMM Gap Trading Algorithm initialized")
# =====================================
# HMM IMPLEMENTATION
# =====================================
def CalculateHMMFeatures(self, prices):
"""
Calculate features for HMM from price data
Returns array of [returns, log_returns, volatility, volume_ratio]
"""
if len(prices) < 2:
return None
# Calculate returns
returns = np.diff(prices) / prices[:-1]
# Calculate log returns (more stable for HMM)
log_returns = np.log(prices[1:] / prices[:-1])
# Rolling volatility (standard deviation of returns)
volatility = pd.Series(returns).rolling(5).std().fillna(method='bfill').values
# Realized volatility (high-frequency measure)
realized_vol = np.abs(returns)
# Create feature matrix
features = np.column_stack([returns, log_returns, volatility, realized_vol])
return features
def TrainHMM(self):
"""
Train Hidden Markov Model on historical data
Uses Gaussian Mixture Model as approximation (available in QC)
"""
# Get historical data
history = self.History(self.spy, self.lookback_days, Resolution.Daily)
if history.empty or len(history) < self.lookback_days:
self.Debug("Insufficient data for HMM training")
return
prices = history['close'].values
features = self.CalculateHMMFeatures(prices)
if features is None or len(features) < 20:
return
# Use Gaussian Mixture Model as HMM approximation
# GMM can identify regimes based on return distributions
self.hmm_model = GaussianMixture(
n_components=self.n_regimes,
covariance_type='full',
max_iter=100,
random_state=42
)
try:
# Fit the model
self.hmm_model.fit(features)
# Get regime sequence
regimes = self.hmm_model.predict(features)
# Identify regime characteristics
self.IdentifyRegimeCharacteristics(features, regimes)
self.Debug("HMM model trained successfully")
except Exception as e:
self.Debug(f"HMM training failed: {str(e)}")
def IdentifyRegimeCharacteristics(self, features, regimes):
"""
Identify which regime is bull/bear/neutral based on returns
"""
regime_returns = {}
for i in range(self.n_regimes):
regime_mask = (regimes == i)
if regime_mask.any():
# Get returns for this regime
regime_data = features[regime_mask, 0] # First column is returns
mean_return = np.mean(regime_data)
regime_returns[i] = mean_return
# Sort regimes by mean return
sorted_regimes = sorted(regime_returns.items(), key=lambda x: x[1])
# Assign: lowest return = bear, highest = bull, middle = neutral
if len(sorted_regimes) == 3:
self.regime_mapping = {
sorted_regimes[0][0]: 0, # Bear
sorted_regimes[1][0]: 1, # Neutral
sorted_regimes[2][0]: 2 # Bull
}
else:
self.regime_mapping = {i: i for i in range(self.n_regimes)}
self.Debug(f"Regime mapping: {self.regime_mapping}")
def DetectCurrentRegime(self):
"""
Detect current market regime using trained HMM
"""
if self.hmm_model is None:
return 1, np.array([0.33, 0.34, 0.33]) # Default to neutral
# Get recent price data
history = self.History(self.spy, self.hmm_features_window, Resolution.Daily)
if history.empty:
return self.current_regime, self.regime_probabilities
prices = history['close'].values
features = self.CalculateHMMFeatures(prices)
if features is None or len(features) == 0:
return self.current_regime, self.regime_probabilities
try:
# Predict current regime
current_regime_raw = self.hmm_model.predict(features[-1:])
# Map to our bull/bear/neutral classification
current_regime = self.regime_mapping.get(current_regime_raw[0], 1)
# Get probability distribution
regime_probs = self.hmm_model.predict_proba(features[-1:])[0]
# Reorder probabilities according to mapping
ordered_probs = np.zeros(self.n_regimes)
for raw_regime, mapped_regime in self.regime_mapping.items():
ordered_probs[mapped_regime] = regime_probs[raw_regime]
return current_regime, ordered_probs
except Exception as e:
self.Debug(f"Regime detection failed: {str(e)}")
return self.current_regime, self.regime_probabilities
def CalculateTransitionProbabilities(self):
"""
Calculate regime transition probabilities from historical sequence
"""
if len(self.regime_history) < 20:
return None
# Count transitions
transitions = np.zeros((self.n_regimes, self.n_regimes))
for i in range(len(self.regime_history) - 1):
from_regime = self.regime_history[i]
to_regime = self.regime_history[i + 1]
transitions[from_regime, to_regime] += 1
# Normalize to get probabilities
for i in range(self.n_regimes):
row_sum = transitions[i].sum()
if row_sum > 0:
transitions[i] /= row_sum
else:
transitions[i] = 1.0 / self.n_regimes
return transitions
# =====================================
# MAIN STRATEGY EXECUTION
# =====================================
def ExecuteStrategy(self):
"""Main execution combining HMM regime detection with gap trading"""
# Step 1: Detect current regime using HMM
self.current_regime, self.regime_probabilities = self.DetectCurrentRegime()
# Store regime history
self.regime_history.append(self.current_regime)
# Step 2: Scan for gap opportunities
self.ScanForGaps()
# Step 3: Execute trades with regime-based allocation
self.ExecuteTradesWithRegimeBias()
# Step 4: Manage existing positions
self.ManageExits()
# Log regime status
regime_name = self.regime_stats[self.current_regime]['name']
confidence = self.regime_probabilities[self.current_regime]
self.Debug(f"Current regime: {regime_name} (confidence: {confidence:.2%})")
def ScanForGaps(self):
"""
Identify gap opportunities with HMM regime adjustment
"""
for kvp in self.CurrentSlice.Bars:
symbol = kvp.Key
bar = kvp.Value
if symbol == self.spy:
continue
# Initialize data storage
if symbol not in self.data:
self.data[symbol] = {'previous_close': None}
self.atr_indicators[symbol] = self.ATR(symbol, self.atr_period)
if self.data[symbol]['previous_close'] is not None:
prev_close = self.data[symbol]['previous_close']
gap = (bar.Open - prev_close) / prev_close
# Adjust thresholds based on HMM regime
if self.current_regime == 2: # Bull regime
# More confident in upward gaps
long_threshold = self.gap_threshold * 0.7
short_threshold = self.gap_threshold * 1.3
elif self.current_regime == 0: # Bear regime
# More confident in downward gaps
long_threshold = self.gap_threshold * 1.3
short_threshold = self.gap_threshold * 0.7
else: # Neutral regime
long_threshold = self.gap_threshold
short_threshold = self.gap_threshold
# Check for gap signals
if gap > long_threshold and symbol not in self.long_positions:
# Additional filter: regime confidence
if self.current_regime != 0 or self.regime_probabilities[0] < 0.7:
self.long_positions[symbol] = {
'entry_time': self.Time,
'gap_size': gap,
'entry_price': bar.Open,
'entry_regime': self.current_regime
}
if symbol in self.short_positions:
del self.short_positions[symbol]
elif gap < -short_threshold and symbol not in self.short_positions:
# Additional filter: regime confidence
if self.current_regime != 2 or self.regime_probabilities[2] < 0.7:
self.short_positions[symbol] = {
'entry_time': self.Time,
'gap_size': gap,
'entry_price': bar.Open,
'entry_regime': self.current_regime
}
if symbol in self.long_positions:
del self.long_positions[symbol]
self.data[symbol]['previous_close'] = bar.Close
def ExecuteTradesWithRegimeBias(self):
"""
Execute trades with HMM regime-based allocation
"""
num_longs = len(self.long_positions)
num_shorts = len(self.short_positions)
if num_longs == 0 and num_shorts == 0:
return
# HMM-based allocation using regime probabilities
bull_prob = self.regime_probabilities[2]
bear_prob = self.regime_probabilities[0]
neutral_prob = self.regime_probabilities[1]
# Dynamic allocation based on regime probabilities
# More nuanced than simple regime classification
long_allocation = self.max_gross_exposure * (0.3 + 0.5 * bull_prob)
short_allocation = self.max_gross_exposure * (0.3 + 0.5 * bear_prob)
# Ensure we don't exceed max exposure
total_allocation = long_allocation + short_allocation
if total_allocation > self.max_gross_exposure:
scale = self.max_gross_exposure / total_allocation
long_allocation *= scale
short_allocation *= scale
# Execute long positions
if num_longs > 0:
weight_per_long = min(long_allocation / num_longs, 0.10)
for symbol in self.long_positions:
self.SetHoldings(symbol, weight_per_long)
self.SetStopLoss(symbol, True)
# Execute short positions
if num_shorts > 0:
weight_per_short = min(short_allocation / num_shorts, 0.10)
for symbol in self.short_positions:
self.SetHoldings(symbol, -weight_per_short)
self.SetStopLoss(symbol, False)
def SetStopLoss(self, symbol, is_long):
"""Set volatility-adjusted stop loss"""
if symbol in self.atr_indicators and self.atr_indicators[symbol].IsReady:
atr = self.atr_indicators[symbol].Current.Value
current_price = self.Securities[symbol].Price
# Adjust stop distance based on regime volatility
regime_vol_multiplier = 1.0 + self.regime_stats[self.current_regime]['volatility']
if is_long:
stop_price = current_price - (2 * atr * regime_vol_multiplier)
self.StopMarketOrder(symbol, -self.Portfolio[symbol].Quantity, stop_price)
else:
stop_price = current_price + (2 * atr * regime_vol_multiplier)
self.StopMarketOrder(symbol, -self.Portfolio[symbol].Quantity, stop_price)
def ManageExits(self):
"""
Exit positions based on:
1. Holding period
2. Regime changes
3. Regime transition probabilities
"""
current_time = self.Time
# Get transition probabilities if available
trans_probs = self.CalculateTransitionProbabilities()
# Check long positions
for symbol in list(self.long_positions.keys()):
position_info = self.long_positions[symbol]
exit = False
# 1. Time-based exit
if current_time - position_info['entry_time'] >= self.holding_period:
exit = True
self.Debug(f"Time exit for long: {symbol}")
# 2. Regime-based exit
if self.current_regime == 0 and self.regime_probabilities[0] > 0.7:
exit = True
self.Debug(f"Bear regime exit for long: {symbol}")
# 3. Regime change exit
if position_info['entry_regime'] == 2 and self.current_regime != 2:
# Entered in bull, no longer bull
exit = True
self.Debug(f"Regime change exit for long: {symbol}")
if exit:
self.Liquidate(symbol)
del self.long_positions[symbol]
# Check short positions
for symbol in list(self.short_positions.keys()):
position_info = self.short_positions[symbol]
exit = False
# 1. Time-based exit
if current_time - position_info['entry_time'] >= self.holding_period:
exit = True
self.Debug(f"Time exit for short: {symbol}")
# 2. Regime-based exit
if self.current_regime == 2 and self.regime_probabilities[2] > 0.7:
exit = True
self.Debug(f"Bull regime exit for short: {symbol}")
# 3. Regime change exit
if position_info['entry_regime'] == 0 and self.current_regime != 0:
# Entered in bear, no longer bear
exit = True
self.Debug(f"Regime change exit for short: {symbol}")
if exit:
self.Liquidate(symbol)
del self.short_positions[symbol]
def OnEndOfAlgorithm(self):
"""Final analysis of regime detection accuracy"""
if len(self.regime_history) > 0:
# Calculate regime distribution
regime_counts = np.bincount(list(self.regime_history), minlength=3)
total = len(self.regime_history)
self.Debug("=== HMM Regime Analysis ===")
self.Debug(f"Bear days: {regime_counts[0]}/{total} ({regime_counts[0]/total:.1%})")
self.Debug(f"Neutral days: {regime_counts[1]}/{total} ({regime_counts[1]/total:.1%})")
self.Debug(f"Bull days: {regime_counts[2]}/{total} ({regime_counts[2]/total:.1%})")
# Calculate transition probabilities
trans_probs = self.CalculateTransitionProbabilities()
# Display regime persistence if available
if trans_probs is not None:
self.Debug("\n=== Regime Persistence (diagonal of transition matrix) ===")
self.Debug(f"Bear persistence: {trans_probs[0,0]:.1%}")
self.Debug(f"Neutral persistence: {trans_probs[1,1]:.1%}")
self.Debug(f"Bull persistence: {trans_probs[2,2]:.1%}")
self.Debug("\n=== Full Transition Matrix ===")
self.Debug(" To: Bear Neutral Bull")
self.Debug(f"From Bear: {trans_probs[0,0]:.2f} {trans_probs[0,1]:.2f} {trans_probs[0,2]:.2f}")
self.Debug(f"From Neutral: {trans_probs[1,0]:.2f} {trans_probs[1,1]:.2f} {trans_probs[1,2]:.2f}")
self.Debug(f"From Bull: {trans_probs[2,0]:.2f} {trans_probs[2,1]:.2f} {trans_probs[2,2]:.2f}")