Created with Highcharts 12.1.2EquityJul 2023Sep 2023Nov 2023Jan 2024Mar 2024May 2024Jul 2024Sep 2024Nov 2024Jan 2025Mar 2025May 202502M4M-20-1000250500-1010500k1,000k0100G200G90100110
Overall Statistics
Total Orders
56651
Average Win
0.09%
Average Loss
-0.10%
Compounding Annual Return
70.669%
Drawdown
21.700%
Expectancy
0.034
Start Equity
1000000
End Equity
2579387.85
Net Profit
157.939%
Sharpe Ratio
1.926
Sortino Ratio
2.299
Probabilistic Sharpe Ratio
89.231%
Loss Rate
46%
Win Rate
54%
Profit-Loss Ratio
0.90
Alpha
0.429
Beta
-0.071
Annual Standard Deviation
0.222
Annual Variance
0.049
Information Ratio
1.63
Tracking Error
0.254
Treynor Ratio
-6.015
Total Fees
â‚®0.00
Estimated Strategy Capacity
â‚®290000.00
Lowest Capacity Asset
BTCUSDT 18N
Portfolio Turnover
8565.73%
#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");
                SetCash(1_000_000);
            }
            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("8NYYxj6tW3lvXBQHQxtHXidK4P6WcaKkwB9cO6UhLA0nm3dYD3Pd+cxPBFoU36/zxP5cfj4LLxMPpNr9Q5tnJke6ZAb20YKkALEg+XmhBD1HlWdIe4cTMRAZJOII9wtr0KIg3kJVi+O7cIgf65UZ3hNyhOQWEcas6AXA2REzkKcTda9Cu4eviWQDzEoga49jJGrgntZlmyu7ZMT9K3XjPFOaXoZWEe7c6A3IGdEzzCfTAdPiu2/zeeQDiPjgdzHzxP583kKlU+vTCizff5WnXFNy0MbWZe7+6A2UGdFP7HXTddPwO2fzyyR3xBhgg29rDKLmfD5dq+O7cIgvawG3/tOSfobWZe6sqHngSxEz9MfTfYdC+2+nqeSXYrrgY5exJGLg/BZtk/k7bOy9K32fvBNyCE5WhT5swB8gmzmn1AeLCbfA+2fDieQDiPggF7+RJB6M3NYZ77n7GPTf633z/tOafobWZe7+KAXgyxFHzCfTAa/w+2enq6R/1JrggxHTpGLIPNYRk5v7ZMTf63W33tN60IbWLeCu");
            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);
                }
            }
        }
    }
}