| Overall Statistics |
|
Total Orders 25150 Average Win 0.12% Average Loss -0.10% Compounding Annual Return 51.024% Drawdown 20.900% Expectancy 0.062 Start Equity 1000000.00 End Equity 2090843.31 Net Profit 109.084% Sharpe Ratio 2.155 Sortino Ratio 3.506 Probabilistic Sharpe Ratio 98.932% Loss Rate 51% Win Rate 49% Profit-Loss Ratio 1.19 Alpha 0.287 Beta -0.014 Annual Standard Deviation 0.133 Annual Variance 0.018 Information Ratio 1.286 Tracking Error 0.192 Treynor Ratio -20.331 Total Fees $0.00 Estimated Strategy Capacity $420000.00 Lowest Capacity Asset BTCUSDT 18N Portfolio Turnover 3777.71% |
#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.02m;
private decimal _exitPositionThreshold = 0.70m;
private decimal _takeProfitTarget = 0.005m;
private decimal _stopLossLevel = 5m;
// Min accuracy threshold for normal operation
private decimal _normalThreshold = 0.48m; // TODO
// Max accuracy threshold for reversed operation
private decimal _reversedThreshold = 0.43m; // 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, 14);
// SetStartDate(2024, 8, 1);
// SetEndDate(2024, 6, 1);
// SetEndDate(2024, 9, 1);
// SetEndDate(2023, 10, 30);
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;
foreach (var pair in predictions)
{
DateTime predictionTime = pair.Key;
PredictionRecord record = pair.Value;
if (record.IsCorrect.HasValue) continue;
DateTime evalStartTime = predictionTime.AddMinutes(_earlyProfitMinHoldingTime);
DateTime evalEndTime = predictionTime.AddMinutes(_earlyProfitMinHoldingTime + _positionHoldingWindow);
if (Time >= evalEndTime)
{
var pricesInWindow = _priceHistory.GetItemsInTimeRange(evalStartTime, evalEndTime);
if (pricesInWindow.Count > 0)
{
decimal avgPrice = pricesInWindow.Average();
bool priceWentUp = avgPrice > record.EntryPrice;
bool predictedUp = record.Probability > 0.5m;
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}.");
}
}
}
UpdateModelState();
}
private void UpdateModelState()
{
var predictions = _predictionRecords.GetItems();
var evaluatedPredictions = predictions.Where(p => p.IsCorrect.HasValue).ToList();
if (evaluatedPredictions.Count < _minPredictionsForAccuracy)
{
Log($"Not enough evaluated predictions ({evaluatedPredictions.Count}/{_minPredictionsForAccuracy}) to determine accuracy");
return;
}
int correctCount = evaluatedPredictions.Count(p => p.IsCorrect.Value);
decimal accuracy = (decimal)correctCount / evaluatedPredictions.Count;
ModelState previousState = _currentModelState;
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);
_predictionRecords.Add(Time, new PredictionRecord
{
Probability = originalPredictProb,
EntryPrice = bar.Close,
IsCorrect = null // Will be evaluated later
});
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/H5sTVqfuMGTirXCkaJHAwzmPJMd7m3c4b4cTQWSXzMLIDaELDP4Qfv4LD0MPMAxvywHD1ovGXoZWEe4+qHHweZFP3McTdafC+xPbS6R/iEog19Pb5IpmntYZu9n7EPRvq3WnLpN68KQWGYqeqHGEm/GvSoVTAbeAOxvL+SQLxBgga6+xJGLo/JZtu/lTjKjXy+EDnke6OPb+CwbcfJHQOfGvSoVTAbdwOxOnyyQDzEogH9sxJB7QvBYZizn72KjXy+FHBicuGF6i0f5eyDGMS1E7xKXzdZfA+2fDieQD7Eoga58RJBbIfNYR7zm7hKjX65UZ3tN6+OQWbca+KHno2RFH7OcTfZ/iO28bQWSXzMKUDU8rEKrg5pbRezP7hGrf63W3LhMO0HaWZd6sKHHg+dFHoMfTdeOiO9OvgeSXYrrgY7/x5BbIvNZl33k7bPRvq3W//BN64Kb2Ze6c6HGUuxE71OfTdbeA+2fDieR3zLrgY18bMML2zA==");
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);
}
}
}
}
}