Overall Statistics |
Total Orders 53828 Average Win 0.09% Average Loss -0.10% Compounding Annual Return 72.336% Drawdown 22.400% Expectancy 0.039 Start Equity 1000000.00 End Equity 2635996.60 Net Profit 163.600% Sharpe Ratio 2.061 Sortino Ratio 2.45 Probabilistic Sharpe Ratio 92.470% Loss Rate 46% Win Rate 54% Profit-Loss Ratio 0.94 Alpha 0.437 Beta -0.061 Annual Standard Deviation 0.211 Annual Variance 0.044 Information Ratio 1.509 Tracking Error 0.254 Treynor Ratio -7.123 Total Fees $0.00 Estimated Strategy Capacity $290000.00 Lowest Capacity Asset BTCUSDT 18N Portfolio Turnover 8109.23% |
#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; } public List<T> GetItemsInTimeRange(DateTime startTime, DateTime endTime) { List<T> result = new List<T>(); for (int i = 0; i < _count; i++) { int index = (_currentIndex - _count + i + _size) % _size; if (_timestamps[index] >= startTime && _timestamps[index] <= endTime) { result.Add(_buffer[index]); } } return result; } } // Prediction record to track accuracy private class PredictionRecord { public decimal Probability { get; set; } public decimal EntryPrice { get; set; } public bool? IsCorrect { get; set; } // null means not yet determined } private enum ModelState { Normal, Reversed, NotReliable, } 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.008m; private decimal _stopLossLevel = 5m; // Min accuracy threshold for normal operation private decimal _normalThreshold = 0.40m; // TODO // Max accuracy threshold for reversed operation private decimal _reversedThreshold = 0.35m; // TODO // Min number of predictions needed to evaluate accuracy private int _minPredictionsForAccuracy = 30; // half in 30, half in 60, // TODO private ModelState _currentModelState = ModelState.Normal; 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 RingBuffer<decimal> _priceHistory; private RingBuffer<PredictionRecord> _predictionRecords; // Records for accuracy tracking private int _maxPredictionHistory => 60 + _positionHoldingWindow + _earlyProfitMinHoldingTime; // TODO 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() { // SetStartDate(2023, 1, 1); SetStartDate(2023, 7, 1); // SetStartDate(2023, 10, 1); // SetStartDate(2024, 8, 1); // SetEndDate(2024, 6, 1); // SetEndDate(2024, 9, 1); // SetEndDate(2023, 7, 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); _priceHistory = new RingBuffer<decimal>(_maxPredictionHistory + _positionHoldingWindow + _earlyProfitMinHoldingTime); _predictionRecords = new RingBuffer<PredictionRecord>(_maxPredictionHistory); // Reload model every 00:00 UTC // Schedule.On( // DateRules.EveryDay("BTCUSDT"), // TimeRules.At(new TimeSpan(00, 00, 00)), // LoadModelParameters // ); // 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 // ); // Schedule evaluation of past predictions Schedule.On( DateRules.EveryDay("BTCUSDT"), TimeRules.Every(TimeSpan.FromMinutes(1)), EvaluatePastPredictions ); ResetStateMachine(); LoadModelParameters(); LoadThresholdArray(); } private void ResetStateMachine() { if (_currentModelState != ModelState.Normal) { Log( $"Resetting state machine. Previous state: {_currentModelState}" ); } _currentModelState = ModelState.Normal; Log( $"State machine reset for {Time.Date:yyyy-MM-dd}. Now in {_currentModelState} state." ); } private void EvaluatePastPredictions() { var predictions = _predictionRecords.GetAllWithTimestamps(); if (predictions.Count == 0) return; // Look through past predictions that need evaluation foreach (var pair in predictions) { DateTime predictionTime = pair.Key; PredictionRecord record = pair.Value; // Skip already evaluated predictions if (record.IsCorrect.HasValue) continue; // Calculate the evaluation window end DateTime evalStartTime = predictionTime.AddMinutes(_earlyProfitMinHoldingTime); DateTime evalEndTime = predictionTime.AddMinutes(_earlyProfitMinHoldingTime + _positionHoldingWindow); // If we're past the evaluation window, check if prediction was correct if (Time >= evalEndTime) { // Get prices from our price history buffer var pricesInWindow = _priceHistory.GetItemsInTimeRange(evalStartTime, evalEndTime); if (pricesInWindow.Count > 0) { // Calculate average price in the window decimal avgPrice = pricesInWindow.Average(); // Determine if prediction was correct bool priceWentUp = avgPrice > record.EntryPrice; bool predictedUp = record.Probability > 0.5m; // Set prediction correctness record.IsCorrect = (predictedUp == priceWentUp); Log($"Evaluated prediction from {predictionTime}: predicted {(predictedUp ? "UP" : "DOWN")}, " + $"actual {(priceWentUp ? "UP" : "DOWN")}, correct: {record.IsCorrect}"); } else { Log($"Warning: No price data found for window {evalStartTime} to {evalEndTime}. Cannot evaluate prediction from {predictionTime}."); } } } // Update model state based on prediction accuracy UpdateModelState(); } private void UpdateModelState() { var predictions = _predictionRecords.GetItems(); // Only evaluated predictions var evaluatedPredictions = predictions.Where(p => p.IsCorrect.HasValue).ToList(); // Need minimum number of predictions to make a determination if (evaluatedPredictions.Count < _minPredictionsForAccuracy) { Log($"Not enough evaluated predictions ({evaluatedPredictions.Count}/{_minPredictionsForAccuracy}) to determine accuracy"); return; } // Calculate accuracy int correctCount = evaluatedPredictions.Count(p => p.IsCorrect.Value); decimal accuracy = (decimal)correctCount / evaluatedPredictions.Count; ModelState previousState = _currentModelState; // Update state based on accuracy if (accuracy >= _normalThreshold) { _currentModelState = ModelState.Normal; } else if (accuracy <= _reversedThreshold) { _currentModelState = ModelState.Reversed; } else { _currentModelState = ModelState.NotReliable; } if (previousState != _currentModelState) { Log($"State transition: {previousState} -> {_currentModelState} based on prediction accuracy of {accuracy:P2} " + $"(correct: {correctCount}/{evaluatedPredictions.Count})"); } } 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]; _priceHistory.Add(Time, bar.Close); decimal[] features = CalculateFeatures(bar); decimal originalPredictProb = PredictProbability(features); decimal adjustedPredictProb = AdjustPredictionByState(originalPredictProb); decimal percentile = GetProbabilityPercentile(adjustedPredictProb); _predictionHistory.Add(Time, originalPredictProb); // Add to prediction records for later accuracy evaluation _predictionRecords.Add(Time, new PredictionRecord { Probability = originalPredictProb, EntryPrice = bar.Close, IsCorrect = null // Will be evaluated later }); // Calculate current prediction accuracy string accuracyStr = "N/A"; var evaluatedPredictions = _predictionRecords.GetItems().Where(p => p.IsCorrect.HasValue).ToList(); if (evaluatedPredictions.Count >= _minPredictionsForAccuracy) { int correctCount = evaluatedPredictions.Count(p => p.IsCorrect.Value); decimal accuracy = (decimal)correctCount / evaluatedPredictions.Count; accuracyStr = $"{accuracy:P2} ({correctCount}/{evaluatedPredictions.Count})"; } Log( $"[OnData] - Time: {Time}, Price: {bar.Close}, Original Prediction: {originalPredictProb:F4}, " + $"Adjusted Prediction: {adjustedPredictProb:F4}, Percentile: {percentile:P2}, State: {_currentModelState}, " + $"Accuracy: {accuracyStr}" ); bool shouldBeLong = percentile >= (1m - _enterPositionThreshold / 2m); bool shouldBeShort = percentile <= (_enterPositionThreshold / 2m); bool shouldExitLong = percentile <= (_exitPositionThreshold / 2m); bool shouldExitShort = percentile >= (1m - _exitPositionThreshold / 2m); // Don't take positions if model is NotReliable if (_currentModelState == ModelState.NotReliable) { shouldBeLong = false; shouldBeShort = false; } 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:F4}%" : stopLossTriggered ? $"Stop loss triggered: {currentPnlPercent:F4}%" : "Exit threshold reached"; ClosePosition( "LONG", bar.Close, reason, originalPredictProb, adjustedPredictProb ); } } 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:F4}%" : stopLossTriggered ? $"Stop loss triggered: {currentPnlPercent:F4}%" : "Exit threshold reached"; ClosePosition( "SHORT", bar.Close, reason, originalPredictProb, adjustedPredictProb ); } } // 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.Reversed: // Invert the prediction (1-p) return 1m - originalPrediction; case ModelState.NotReliable: // Just return 0.5 (no clear signal) return 0.5m; default: return originalPrediction; } } 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:F4}%, 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+qHngS5FP9MeTdb9wO2/LKyR/xMpgg7/TZB7IbhZtkyv7GPT96wHjHBMOtPbWEfasKJGss1FHxMXTdbcw+xPz6+QL5HjgY5/TpGrYPBYRu2tTjKjXy+EDnke6OPb+CwbcfJHQOfGvSoVTAbeAOxvreSQD7BjgY9sRJBbIfJZt7/n70KjXy+FHBicuGF6i0f5eyDGMS1E7xKXzdZfA+2fDieQDqPigY5+RpGKcnhYZ71m7hKjX65UZ3tN6+OQWbca+KHno2RFH7OcTfZ/iO28bQWSXzMKUDU8rEKrg5pbRezP7hGrf63W3rhMGtPaWZcb+6AXImRFPgMcTCYdwe4fDiWQDzLrgY/tx5GrQ3JZti2v7ZJC9Kwmv/JNytKb2Ze6c6HGUu5FHgPXTAafw+xPT+eQD3MrgF08bMML2zA=="); 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); } } } } }