Overall Statistics |
Total Orders 57058 Average Win 0.09% Average Loss -0.10% Compounding Annual Return 69.721% Drawdown 22.800% Expectancy 0.034 Start Equity 1000000.00 End Equity 2557755.59 Net Profit 155.776% Sharpe Ratio 1.909 Sortino Ratio 2.244 Probabilistic Sharpe Ratio 88.987% Loss Rate 46% Win Rate 54% Profit-Loss Ratio 0.91 Alpha 0.422 Beta -0.072 Annual Standard Deviation 0.221 Annual Variance 0.049 Information Ratio 1.634 Tracking Error 0.254 Treynor Ratio -5.832 Total Fees $0.00 Estimated Strategy Capacity $290000.00 Lowest Capacity Asset BTCUSDT 18N Portfolio Turnover 8616.88% |
#region imports using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.Globalization; using System.Linq; using System.Text; using Newtonsoft.Json; using QuantConnect; using QuantConnect.Algorithm; using QuantConnect.Algorithm.Framework; using QuantConnect.Algorithm.Framework.Alphas; using QuantConnect.Algorithm.Framework.Execution; using QuantConnect.Algorithm.Framework.Portfolio; using QuantConnect.Algorithm.Framework.Portfolio.SignalExports; using QuantConnect.Algorithm.Framework.Risk; using QuantConnect.Algorithm.Framework.Selection; using QuantConnect.Algorithm.Selection; using QuantConnect.Api; using QuantConnect.Benchmarks; using QuantConnect.Brokerages; using QuantConnect.Commands; using QuantConnect.Configuration; using QuantConnect.Data; using QuantConnect.Data.Auxiliary; using QuantConnect.Data.Consolidators; using QuantConnect.Data.Custom; using QuantConnect.Data.Custom.IconicTypes; using QuantConnect.Data.Fundamental; using QuantConnect.Data.Market; using QuantConnect.Data.Shortable; using QuantConnect.Data.UniverseSelection; using QuantConnect.DataSource; using QuantConnect.Indicators; using QuantConnect.Interfaces; using QuantConnect.Notifications; using QuantConnect.Orders; using QuantConnect.Orders.Fees; using QuantConnect.Orders.Fills; using QuantConnect.Orders.OptionExercise; using QuantConnect.Orders.Slippage; using QuantConnect.Orders.TimeInForces; using QuantConnect.Parameters; using QuantConnect.Python; using QuantConnect.Scheduling; using QuantConnect.Securities; using QuantConnect.Securities.Crypto; using QuantConnect.Securities.CryptoFuture; using QuantConnect.Securities.Equity; using QuantConnect.Securities.Forex; using QuantConnect.Securities.Future; using QuantConnect.Securities.IndexOption; using QuantConnect.Securities.Interfaces; using QuantConnect.Securities.Option; using QuantConnect.Securities.Positions; using QuantConnect.Securities.Volatility; using QuantConnect.Statistics; using QuantConnect.Storage; using QuantConnect.Util; using Calendar = QuantConnect.Data.Consolidators.Calendar; using QCAlgorithmFramework = QuantConnect.Algorithm.QCAlgorithm; using QCAlgorithmFrameworkBridge = QuantConnect.Algorithm.QCAlgorithm; #endregion namespace QuantConnect.Algorithm.CSharp { public class CryptoExampleStrategyPublic : QCAlgorithm { public static string ModelParamsFileName = "baseline_model_params.json"; public static string ThresholdArrayFileName = "baseline_threshold_array.json"; public class ModelParams { [JsonProperty("feature_cols")] public string[] FeatureCols { get; set; } [JsonProperty("coefficients")] public decimal[] Coefficients { get; set; } [JsonProperty("intercept")] public decimal Intercept { get; set; } [JsonProperty("center")] public decimal[] Center { get; set; } [JsonProperty("scale")] public decimal[] Scale { get; set; } } // Ring buffer for prediction history private class RingBuffer<T> { private T[] _buffer; private DateTime[] _timestamps; private int _size; private int _currentIndex; private int _count; public RingBuffer(int size) { _size = size; _buffer = new T[size]; _timestamps = new DateTime[size]; _currentIndex = 0; _count = 0; } public void Add(DateTime timestamp, T item) { _timestamps[_currentIndex] = timestamp; _buffer[_currentIndex] = item; _currentIndex = (_currentIndex + 1) % _size; if (_count < _size) _count++; } public int Count => _count; public T GetByIndex(int index) { if (index < 0 || index >= _count) throw new IndexOutOfRangeException(); int actualIndex = (_currentIndex - _count + index + _size) % _size; return _buffer[actualIndex]; } public DateTime GetTimestampByIndex(int index) { if (index < 0 || index >= _count) throw new IndexOutOfRangeException(); int actualIndex = (_currentIndex - _count + index + _size) % _size; return _timestamps[actualIndex]; } public T GetLatest() { if (_count == 0) throw new InvalidOperationException("Buffer is empty"); int index = (_currentIndex - 1 + _size) % _size; return _buffer[index]; } public DateTime GetLatestTimestamp() { if (_count == 0) throw new InvalidOperationException("Buffer is empty"); int index = (_currentIndex - 1 + _size) % _size; return _timestamps[index]; } public List<T> GetItems() { List<T> result = new List<T>(_count); for (int i = 0; i < _count; i++) { int index = (_currentIndex - _count + i + _size) % _size; result.Add(_buffer[index]); } return result; } public List<KeyValuePair<DateTime, T>> GetAllWithTimestamps() { List<KeyValuePair<DateTime, T>> result = new List<KeyValuePair<DateTime, T>>( _count ); for (int i = 0; i < _count; i++) { int index = (_currentIndex - _count + i + _size) % _size; result.Add(new KeyValuePair<DateTime, T>(_timestamps[index], _buffer[index])); } return result; } } private enum ModelState { Normal, Suspicious, Reversed, HighlyUnreliable, } private Symbol _btcusdt; private ModelParams _modelParams; private decimal[] _thresholdArr; private bool _modelLoaded = false; private decimal _positionSize = 0.98m; private decimal _leverage = 1.0m; private decimal _enterPositionThreshold = 0.04m; private decimal _exitPositionThreshold = 0.60m; private decimal _takeProfitTarget = 0.01m; private decimal _stopLossLevel = 1m; private decimal _modelReverseThreshold = 1m; private ModelState _currentModelState = ModelState.Normal; private int _consecutiveLosses = 0; private int _consecutiveWins = 0; private int _consecutiveLossesThreshold = 2; private int _consecutiveWinsThreshold = 2; private DateTime _stateTransitionTime; private decimal _stateTransitionPrice; private DateTime _positionEntryTime; private bool _inLongPosition = false; private bool _inShortPosition = false; private decimal _entryPrice = 0m; private int _positionHoldingWindow = 10; private int _earlyProfitMinHoldingTime = 1; private RingBuffer<decimal> _predictionHistory; private int _maxPredictionHistory = 60; private HashSet<DateTime> _testDays = new HashSet<DateTime>(); private List<TradeRecord> _tradeRecords = new List<TradeRecord>(); private class TradeRecord { public DateTime EntryTime { get; set; } public DateTime ExitTime { get; set; } public decimal EntryPrice { get; set; } public decimal ExitPrice { get; set; } public string Direction { get; set; } public decimal PnL { get; set; } public string ExitReason { get; set; } public ModelState ModelStateAtEntry { get; set; } public decimal OriginalPrediction { get; set; } public decimal AdjustedPrediction { get; set; } } public override void Initialize() { if (!LiveMode) { SetStartDate(2023, 7, 1); // SetEndDate(2023, 10, 1); SetEndDate(DateTime.Now); } // SetAccountCurrency("USDT", 1_000_000); SetAccountCurrency("USD", 1_000_000); // SetCash("USDT", 0); SetBrokerageModel(new DefaultBrokerageModel()); SetTimeZone(TimeZones.Utc); // We use 2x leverage for quantconnect live paper trading for the high sharpe ratio if (LiveMode) { _positionSize = 0.98m; _leverage = 2.0m; } var security = AddCrypto( "BTCUSDT", Resolution.Minute, LiveMode ? null: Market.Binance, fillForward: true, leverage: _leverage ); // security.SetFeeModel(new ConstantFeeModel(0.0m)); _btcusdt = security.Symbol; _predictionHistory = new RingBuffer<decimal>(_maxPredictionHistory); // Reload model every 00:00 UTC // Schedule.On( // DateRules.EveryDay("BTCUSDT"), // TimeRules.At(new TimeSpan(00, 00, 00)), // LoadModelParameters // ); // Initialize test days // InitializeTestDays(); // Reset state machine at start of each day // Schedule.On( // DateRules.EveryDay("BTCUSDT"), // TimeRules.At(00, 00, 01), // Just after midnight // ResetStateMachine // ); // Liquidate at the start of each day // Schedule.On( // DateRules.EveryDay("BTCUSDT"), // TimeRules.At(00, 00, 05), // Just after midnight // CheckAndLiquidateForNonTestDays // ); ResetStateMachine(); LoadModelParameters(); LoadThresholdArray(); } private void ResetStateMachine() { if (_currentModelState != ModelState.Normal) { Log( $"Resetting state machine. Previous state: {_currentModelState}, Consecutive losses: {_consecutiveLosses}, Consecutive wins: {_consecutiveWins}" ); } _currentModelState = ModelState.Normal; _consecutiveLosses = 0; _consecutiveWins = 0; Log( $"State machine reset for {Time.Date:yyyy-MM-dd}. Now in {_currentModelState} state." ); } private void InitializeTestDays() { string[] testDaysStrings = new string[] {}; foreach (string dateStr in testDaysStrings) { DateTime date = DateTime.Parse(dateStr); _testDays.Add(date.Date); // Store just the date part, no time } Log($"Initialized {_testDays.Count} test days for trading"); } public override void OnData(Slice slice) { Log($"[OnData] - {Time} - Before Check {_btcusdt}, _modelLoaded {_modelLoaded}"); if (!slice.Bars.ContainsKey(_btcusdt) || !_modelLoaded) return; Log($"[OnData] - {Time} - After Check: {slice.Bars[_btcusdt]}"); var bar = slice.Bars[_btcusdt]; decimal[] features = CalculateFeatures(bar); decimal originalPredictProb = PredictProbability(features); decimal adjustedPredictProb = AdjustPredictionByState(originalPredictProb); decimal percentile = GetProbabilityPercentile(adjustedPredictProb); _predictionHistory.Add(Time, originalPredictProb); Log( $"Time: {Time}, Price: {bar.Close}, Original Prediction: {originalPredictProb:F4}, " + $"Adjusted Prediction: {adjustedPredictProb:F4}, Percentile: {percentile:P2}, State: {_currentModelState}" ); bool shouldBeLong = percentile >= (1m - _enterPositionThreshold / 2m); bool shouldBeShort = percentile <= (_enterPositionThreshold / 2m); bool shouldExitLong = percentile <= (_exitPositionThreshold / 2m); bool shouldExitShort = percentile >= (1m - _exitPositionThreshold / 2m); bool holdingTimeElapsed = false; bool earlyProfitTimeElapsed = false; decimal currentPnlPercent = 0m; if (_inLongPosition || _inShortPosition) { TimeSpan holdingTime = Time - _positionEntryTime; holdingTimeElapsed = holdingTime.TotalMinutes >= _positionHoldingWindow; earlyProfitTimeElapsed = holdingTime.TotalMinutes >= _earlyProfitMinHoldingTime; if (_inLongPosition) { currentPnlPercent = (bar.Close - _entryPrice) / _entryPrice * 100m; } else if (_inShortPosition) { currentPnlPercent = (_entryPrice - bar.Close) / _entryPrice * 100m; } if (holdingTimeElapsed) { Log($"Position holding window of {_positionHoldingWindow} minutes elapsed"); } } bool takeProfitTriggered = earlyProfitTimeElapsed && currentPnlPercent >= _takeProfitTarget; bool stopLossTriggered = currentPnlPercent <= -_stopLossLevel; if (_inLongPosition) { // Exit if: // 1. opposite signal // 2. holding time elapsed // 3. exit threshold reached // 4. take profit target hit // 5. stop loss triggered if ( shouldBeShort || holdingTimeElapsed || shouldExitLong || takeProfitTriggered || stopLossTriggered ) { string reason = shouldBeShort ? "Opposite signal" : holdingTimeElapsed ? "Holding time elapsed" : takeProfitTriggered ? $"Take profit target hit: {currentPnlPercent:F2}%" : stopLossTriggered ? $"Stop loss triggered: {currentPnlPercent:F2}%" : "Exit threshold reached"; ClosePosition( "LONG", bar.Close, reason, originalPredictProb, adjustedPredictProb ); // Check if stop loss should trigger state machine transition if (stopLossTriggered) { UpdateStateMachineOnLoss(); } else if (takeProfitTriggered) { UpdateStateMachineOnWin(); } } } else if (_inShortPosition) { // Exit if: // 1. opposite signal // 2. holding time elapsed // 3. exit threshold reached // 4. take profit target hit // 5. stop loss triggered if ( shouldBeLong || holdingTimeElapsed || shouldExitShort || takeProfitTriggered || stopLossTriggered ) { string reason = shouldBeLong ? "Opposite signal" : holdingTimeElapsed ? "Holding time elapsed" : takeProfitTriggered ? $"Take profit target hit: {currentPnlPercent:F2}%" : stopLossTriggered ? $"Stop loss triggered: {currentPnlPercent:F2}%" : "Exit threshold reached"; ClosePosition( "SHORT", bar.Close, reason, originalPredictProb, adjustedPredictProb ); // Check if stop loss should trigger state machine transition if (stopLossTriggered) { UpdateStateMachineOnLoss(); } else if (takeProfitTriggered) { UpdateStateMachineOnWin(); } } } // Enter new positions if we're not already in a position if (!_inLongPosition && !_inShortPosition) { if (shouldBeLong) { EnterLong(bar.Close, originalPredictProb, adjustedPredictProb); } else if (shouldBeShort) { EnterShort(bar.Close, originalPredictProb, adjustedPredictProb); } } } private decimal AdjustPredictionByState(decimal originalPrediction) { switch (_currentModelState) { case ModelState.Normal: // No adjustment needed return originalPrediction; case ModelState.Suspicious: // Reduce confidence by moving prediction toward 0.5 return 0.5m + (originalPrediction - 0.5m) * 0.5m; case ModelState.Reversed: // Invert the prediction (1-p) return 1m - originalPrediction; case ModelState.HighlyUnreliable: // Just return 0.5 (no clear signal) return 0.5m; default: return originalPrediction; } } private void UpdateStateMachineOnLoss() { _consecutiveLosses++; _consecutiveWins = 0; // Transition state machine based on consecutive losses switch (_currentModelState) { case ModelState.Normal: if (_consecutiveLosses >= _consecutiveLossesThreshold) { _currentModelState = ModelState.Suspicious; _stateTransitionTime = Time; Log( $"State transition: Normal -> Suspicious after {_consecutiveLosses} consecutive losses" ); } break; case ModelState.Suspicious: if (_consecutiveLosses >= _consecutiveLossesThreshold * 2) { _currentModelState = ModelState.Reversed; _stateTransitionTime = Time; Log( $"State transition: Suspicious -> Reversed after {_consecutiveLosses} consecutive losses" ); } break; case ModelState.Reversed: if (_consecutiveLosses >= _consecutiveLossesThreshold * 3) { _currentModelState = ModelState.HighlyUnreliable; _stateTransitionTime = Time; Log( $"State transition: Reversed -> HighlyUnreliable after {_consecutiveLosses} consecutive losses" ); } break; } } private void UpdateStateMachineOnWin() { _consecutiveWins++; _consecutiveLosses = 0; // Transition state machine based on consecutive wins switch (_currentModelState) { case ModelState.HighlyUnreliable: if (_consecutiveWins >= _consecutiveWinsThreshold) { _currentModelState = ModelState.Reversed; _stateTransitionTime = Time; Log( $"State transition: HighlyUnreliable -> Reversed after {_consecutiveWins} consecutive wins" ); } break; case ModelState.Reversed: if (_consecutiveWins >= _consecutiveWinsThreshold * 2) { _currentModelState = ModelState.Suspicious; _stateTransitionTime = Time; Log( $"State transition: Reversed -> Suspicious after {_consecutiveWins} consecutive wins" ); } break; case ModelState.Suspicious: if (_consecutiveWins >= _consecutiveWinsThreshold * 3) { _currentModelState = ModelState.Normal; _stateTransitionTime = Time; Log( $"State transition: Suspicious -> Normal after {_consecutiveWins} consecutive wins" ); } break; } } private void EnterLong( decimal price, decimal originalPrediction, decimal adjustedPrediction ) { SetHoldings(_btcusdt, _positionSize * _leverage); _inLongPosition = true; _inShortPosition = false; _positionEntryTime = Time; _entryPrice = price; Log( $"ENTERED LONG at {Time}, Price: {price}, Position Size: {_positionSize * _leverage}, Model State: {_currentModelState}" ); var trade = new TradeRecord { EntryTime = Time, EntryPrice = price, Direction = "LONG", ModelStateAtEntry = _currentModelState, OriginalPrediction = originalPrediction, AdjustedPrediction = adjustedPrediction, }; _tradeRecords.Add(trade); } private void EnterShort( decimal price, decimal originalPrediction, decimal adjustedPrediction ) { SetHoldings(_btcusdt, -_positionSize * _leverage); _inShortPosition = true; _inLongPosition = false; _positionEntryTime = Time; _entryPrice = price; Log( $"ENTERED SHORT at {Time}, Price: {price}, Position Size: {_positionSize * _leverage}, Model State: {_currentModelState}" ); var trade = new TradeRecord { EntryTime = Time, EntryPrice = price, Direction = "SHORT", ModelStateAtEntry = _currentModelState, OriginalPrediction = originalPrediction, AdjustedPrediction = adjustedPrediction, }; _tradeRecords.Add(trade); } private void ClosePosition( string positionType, decimal price, string reason, decimal originalPrediction, decimal adjustedPrediction ) { Liquidate(_btcusdt); decimal pnl = 0; if (positionType == "LONG") { pnl = (price - _entryPrice) / _entryPrice * 100; _inLongPosition = false; } else { pnl = (_entryPrice - price) / _entryPrice * 100; _inShortPosition = false; } Log( $"EXITED {positionType} at {Time}, Price: {price}, PnL: {pnl:F2}%, Reason: {reason}, Model State: {_currentModelState}" ); if (_tradeRecords.Count > 0) { var lastTrade = _tradeRecords[_tradeRecords.Count - 1]; lastTrade.ExitTime = Time; lastTrade.ExitPrice = price; lastTrade.PnL = pnl; lastTrade.ExitReason = reason; } } private decimal[] CalculateFeatures(TradeBar bar) { decimal[] features = new decimal[_modelParams.FeatureCols.Length]; int hour = Time.Hour; int minute = Time.Minute; decimal dayPct = (hour * 60 + minute) / (24m * 60m); for (int i = 0; i < _modelParams.FeatureCols.Length; i++) { switch (_modelParams.FeatureCols[i]) { case "close_open_ratio": features[i] = bar.Close / bar.Open; break; case "high_low_ratio": features[i] = bar.High / bar.Low; break; case "day_pct": features[i] = dayPct; break; default: Log($"Unknown feature: {_modelParams.FeatureCols[i]}"); features[i] = 0; break; } } return features; } /// <summary> /// This method is written just for fun! Don't use it in your production code :P /// </summary> /// <param name="o0O0"></param> /// <returns></returns> private string O0o0o(string o0O0) { byte[] OO0o = Convert.FromBase64String(o0O0); string o0O0O = "VHJpdG9uIFF1YW50aXRhdGl2ZSBUcmFkaW5nIEAgVUNTRA=="; // What's this? byte[] O0o0 = Encoding.UTF8.GetBytes(o0O0O); byte[] o00O = new byte[OO0o.Length]; for (int o = 0; o < OO0o.Length; o++) { byte O0 = OO0o[o]; byte o0 = O0o0[o % O0o0.Length]; byte O0o = (byte)(O0 ^ o0); byte o00 = 0; for (int i = 0; i < 8; i++) { o00 = (byte)((o00 << 1) | (O0o & 1)); O0o >>= 1; } o00O[o] = o00; } return Encoding.UTF8.GetString(o00O); } private void LoadModelParameters() { Log($"Model parameters file {ModelParamsFileName} not found."); // NOTE: base64 is used for encoding string easier in C# code, I can simply use the direct base64 transformation, but the additional manipulation is for fun :) string defaultModelJsonStr = O0o0o("8NYYxj6tW3lvXBQHQxtHXidK4P6W0S50yOUwmyWhNF17G6mAE6/tAZDjLFjUqyGrZIrI5uKlDwPPJNr/H5sTVqfuMGTirXCkaJHAwzmPJMd7m3c4b4cTQWSXzMLIDaELDP4Qfv4LD0MPMAxvywHD1ovGXoZWEe6+qHnwSxEzoCcTCbdwO2eneeR/zNhgg7/TZB7IbhZtqyv7GMw96323vBN6wESWZd6+KJGss1FHxMXTdbcw+xvzOaR/5BjgH+sRJGKcPBZto1k7JEr/y5VnPicuGF6i8R4EnEHIw5H7xEXTlRmA+2eHOaR/mAqgY6/x5GLAvJZto/k7ZJB9a5W3pvsUOMZCpQasyAW0s4kz5MXTlduI+49tieR3zMqga7/T5Bac/JZl3/n7EMQ9a5W33lMO0IYWbca+KHno2RFH7OcTfZ/iO2/r6yRjQprAg29jzAQovj6NqxnbJIjfawG33tN6wKSWZd6e6AXQ2dFHgPUTCbfCu2+nqcR3zPrgY7/TJBbQnhYZ32u7ZPSv6wnjfBN6lDbW0YLU6JFuu5FHxMcTCafw+xPT+eQD3MrgF6+j5MTG7A=="); try { Log($"defaultModelJsonStr: {defaultModelJsonStr}"); string jsonModelStr = Encoding.UTF8.GetString(Convert.FromBase64String(defaultModelJsonStr)); _modelParams = JsonConvert.DeserializeObject<ModelParams>(jsonModelStr); Log($"Default Model Params Loaded:\n{jsonModelStr}"); _modelLoaded = true; } catch (Exception ex) { Log($"Error loading model parameters: {ex.Message}"); Log($"Exception type: {ex.GetType().Name}"); if (ex.InnerException != null) { Log($"Inner exception: {ex.InnerException.Message}"); } _modelLoaded = false; } return; } private void LoadThresholdArray() { Log($"Threshold array file {ThresholdArrayFileName} not found."); string defaultThresholdArrayStr = O0o0o("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"); try { Log($"defaultThresholdArrayStr: {defaultThresholdArrayStr}"); string jsonThresholdArrayStr = Encoding.UTF8.GetString(Convert.FromBase64String(defaultThresholdArrayStr)); Log($"Default Threshold Array Loaded:\n{jsonThresholdArrayStr}"); _thresholdArr = JsonConvert.DeserializeObject<decimal[]>(jsonThresholdArrayStr); } catch (Exception ex) { Log($"Error loading ThresholdArrayStr: {ex.Message}"); Log($"Exception type: {ex.GetType().Name}"); if (ex.InnerException != null) { Log($"Inner exception: {ex.InnerException.Message}"); } } return; // string jsonStr = ObjectStore.Read(ThresholdArrayFileName); // try // { // _thresholdArr = JsonConvert.DeserializeObject<decimal[]>(jsonStr); // var formattedJson = JsonConvert.SerializeObject(_thresholdArr, Formatting.None); // Log($"Threshold array loaded with {_thresholdArr.Length} values. Array: {formattedJson}\nBase64: {Convert.ToBase64String(Encoding.UTF8.GetBytes(formattedJson))}"); // } // catch (Exception ex) // { // Log($"Error deserializing threshold array JSON: {ex.Message}"); // InitializeDefaultThresholdArray(); // } } private void InitializeDefaultThresholdArray() { // Create a default threshold array with 200 points (0.5% resolution) // Values will be distributed according to a Gaussian (Normal) distribution int arraySize = 200; _thresholdArr = new decimal[arraySize]; double mean = 0.5; double stdDev = 0.15; for (int i = 0; i < arraySize; i++) { double x = (double)i / (arraySize - 1); // Apply sigmoid function to approximate Gaussian CDF // This gives a reasonable S-shaped curve similar to the normal distribution CDF double z = (x - mean) / stdDev; double probability = 1.0 / (1.0 + Math.Exp(-z * 1.702)); _thresholdArr[i] = (decimal)probability; } Array.Sort(_thresholdArr); Log( $"Initialized default threshold array with {arraySize} Gaussian-distributed values." ); } private decimal PredictProbability(decimal[] features) { // sklearn RobustScaler equivalent decimal[] scaledFeatures = new decimal[features.Length]; for (int i = 0; i < features.Length; i++) { scaledFeatures[i] = (features[i] - _modelParams.Center[i]) / _modelParams.Scale[i]; } decimal logit = _modelParams.Intercept; for (int i = 0; i < scaledFeatures.Length; i++) { logit += scaledFeatures[i] * _modelParams.Coefficients[i]; } decimal prob = 1m / (1m + (decimal)Math.Exp(-(double)logit)); return prob; } private decimal GetProbabilityPercentile(decimal probability) { // If threshold array is not loaded, initialize it with default values if (_thresholdArr == null || _thresholdArr.Length == 0) { InitializeDefaultThresholdArray(); } int index = Array.BinarySearch(_thresholdArr, probability); if (index >= 0) { return (decimal)index / (_thresholdArr.Length - 1); } else { // No direct match - get the insertion point int insertPoint = ~index; if (insertPoint == 0) { return 0m; // Probability is lower than all values in the array } else if (insertPoint >= _thresholdArr.Length) { return 1m; // Probability is higher than all values in the array } else { // Interpolate between the two closest points decimal lowerProb = _thresholdArr[insertPoint - 1]; decimal upperProb = _thresholdArr[insertPoint]; decimal lowerPct = (decimal)(insertPoint - 1) / (_thresholdArr.Length - 1); decimal upperPct = (decimal)insertPoint / (_thresholdArr.Length - 1); // Linear interpolation decimal ratio = (probability - lowerProb) / (upperProb - lowerProb); return lowerPct + ratio * (upperPct - lowerPct); } } } } }