Supertrend Put Call Ratio
取引規則
StockSharp 標準 SuperTrend(Period、Multiplier)を使います。空から Close > SuperTrend かつ PCR < 直前平均 − PCRMultiplier × σ で買い、Close < SuperTrend かつ PCR > 直前平均 + PCRMultiplier × σ で売ります。PCR は観測した put 出来高 / call 出来高です。買いは Close < SuperTrend、売りは Close > SuperTrend で PCR に関係なく全決済します。等値は信号なしです。
現在値を除く直前 N 個の連続観測から平均と標本標準偏差を計算します:σ = sqrt(Σ(x − mean)² / (N − 1))。窓全体が必要で、乱数や価格による代替値は生成しません。
入力データ
必須 CSV のヘッダーは SecurityId,Time,Value。Time は信号足の UTC 開始時刻で、形式は yyyy-MM-ddTHH:mm:ssZ。実測値は有限・非負で、時刻ごとに一意です。現在一致する観測だけを使います。欠損時は窓を消去して再準備まで新規取引を止めますが、決済信号は有効です。
パラメーター
| パラメーター | 既定値 |
|---|---|
Period |
10 |
Multiplier |
3 |
PCRPeriod |
20 |
PCRMultiplier |
2 |
PCRHistoryFile |
必須 |
AtrPeriod |
14 |
StopLossAtr |
2 |
CandleType |
TimeSpan.FromMinutes(15).TimeFrame() |
約定と保有数量
AtrPeriod = 14 は Wilder ATR です。空ポジションから入る際、ATR(信号) × StopLossAtr の距離を固定します。ローカルのネイティブ成行保護は気配を監視し、実約定価格を基準に足の間でも決済できます。後の ATR 変化でストップは動きません。ゼロは新しい実行に対して保護を無効化します。逆信号は実残高全体を閉じ、同じ判断で反転・積み増ししません。
検証
約定検証には既存 BTC/TON アーカイブのみを使います。明示した財務報告、オプション観測、準備価格は仕組みのテスト用で、実企業財務やオプション履歴ではありません。短縮期間、二分位、三月の年次調整は明示的な検証設定で、元の長期既定値の取引を証明しません。収益率は主張しません。
namespace StockSharp.Samples.Strategies;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
/// <summary>SuperTrend with observed put/call volume ratio extremes and frozen ATR protection.</summary>
public class SupertrendWithPutCallRatioStrategy : Strategy
{
private readonly StrategyParam<int> _period;
private readonly StrategyParam<decimal> _multiplier;
private readonly StrategyParam<int> _pCRPeriod;
private readonly StrategyParam<decimal> _pCRMultiplier;
private readonly StrategyParam<string> _pCRHistoryFile;
private readonly StrategyParam<int> _atrPeriod;
private readonly StrategyParam<decimal> _stopLossAtr;
private readonly StrategyParam<DataType> _candleType;
private readonly Dictionary<DateTime, decimal> _observations = [];
private readonly Queue<decimal> _history = [];
private Order _signalOrder;
/// <summary>SuperTrend Wilder ATR length.</summary>
public int Period { get => _period.Value; set => _period.Value = value; }
/// <summary>SuperTrend band ATR multiplier.</summary>
public decimal Multiplier { get => _multiplier.Value; set => _multiplier.Value = value; }
/// <summary>Prior consecutive PCR observations used for the threshold.</summary>
public int PCRPeriod { get => _pCRPeriod.Value; set => _pCRPeriod.Value = value; }
/// <summary>Sample standard deviation multiplier for PCR extremes.</summary>
public decimal PCRMultiplier { get => _pCRMultiplier.Value; set => _pCRMultiplier.Value = value; }
/// <summary>CSV observed put volume divided by call volume, keyed by candle UTC open time.</summary>
public string PCRHistoryFile { get => _pCRHistoryFile.Value; set => _pCRHistoryFile.Value = value; }
/// <summary>Wilder ATR length for entry protection.</summary>
public int AtrPeriod { get => _atrPeriod.Value; set => _atrPeriod.Value = value; }
/// <summary>Entry-frozen native stop distance in ATR multiples; zero disables it for a fresh run.</summary>
public decimal StopLossAtr { get => _stopLossAtr.Value; set => _stopLossAtr.Value = value; }
/// <summary>Completed signal candle type.</summary>
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
/// <summary>Initialize the signal and protection parameters.</summary>
public SupertrendWithPutCallRatioStrategy()
{
_period = Param(nameof(Period), 10).SetGreaterThanZero().SetOptimize(5, 20, 3)
.SetDisplay("Period", "SuperTrend Wilder ATR length", "Parameters");
_multiplier = Param(nameof(Multiplier), 3m).SetGreaterThanZero().SetOptimize(2m, 4m, 0.5m)
.SetDisplay("Multiplier", "SuperTrend band ATR multiplier", "Parameters");
_pCRPeriod = Param(nameof(PCRPeriod), 20).SetRange(2, 100000).SetOptimize(10, 30, 5)
.SetDisplay("PCRPeriod", "Prior consecutive PCR observations used for the threshold", "Parameters");
_pCRMultiplier = Param(nameof(PCRMultiplier), 2m).SetGreaterThanZero().SetOptimize(1m, 3m, 0.5m)
.SetDisplay("PCRMultiplier", "Sample standard deviation multiplier for PCR extremes", "Parameters");
_pCRHistoryFile = Param<string>(nameof(PCRHistoryFile)).SetRequired()
.SetDisplay("PCRHistoryFile", "CSV observed put volume divided by call volume, keyed by candle UTC open time", "Parameters");
_atrPeriod = Param(nameof(AtrPeriod), 14).SetGreaterThanZero()
.SetDisplay("AtrPeriod", "Wilder ATR length for entry protection", "Parameters");
_stopLossAtr = Param(nameof(StopLossAtr), 2m).SetRange(0m, decimal.MaxValue)
.SetDisplay("StopLossAtr", "Entry-frozen native stop distance in ATR multiples; zero disables it for a fresh run", "Parameters");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
.SetDisplay("CandleType", "Completed signal candle type", "Parameters");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities() => [(Security, CandleType)];
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_observations.Clear();
_history.Clear();
_signalOrder = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
LoadObservations();
base.OnStarted2(time);
var signal = new SuperTrend { Length = Period, Multiplier = Multiplier };
var atr = new AverageTrueRange { Length = AtrPeriod };
var subscription = SubscribeCandles(CandleType);
subscription.BindEx(signal, atr, ProcessCandle, allowEmpty: true).Start();
foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
{
var quote = new Subscription(DataType.Level1, Security);
quote.MarketData.BuildField = field;
SubscribeLevel1(quote).Start();
}
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, signal);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue signalValue, IIndicatorValue atrValue)
{
if (candle.State != CandleStates.Finished) return;
var ready = _observations.TryGetValue(candle.OpenTime, out var observation);
var low = false;
var high = false;
if (ready && _history.Count == PCRPeriod)
{
var mean = _history.Average();
var deviation = (decimal)Math.Sqrt((double)(_history.Sum(x => (x - mean) * (x - mean)) / (_history.Count - 1)));
low = observation < mean - PCRMultiplier * deviation;
high = observation > mean + PCRMultiplier * deviation;
}
if (!ready) _history.Clear();
else
{
_history.Enqueue(observation);
while (_history.Count > PCRPeriod) _history.Dequeue();
}
if (signalValue.IsEmpty || !signalValue.IsFormed) return;
var current = signalValue.GetValue<decimal>();
var up = candle.ClosePrice > current;
var down = candle.ClosePrice < current;
if (!IsFormedAndOnlineAndAllowTrading() || _signalOrder is { State: not (OrderStates.Done or OrderStates.Failed) }) return;
if ((Position > 0m && down) || (Position < 0m && up))
{
Submit(Position > 0m ? Sides.Sell : Sides.Buy, Math.Abs(Position), "0341Exit");
return;
}
if (Position != 0m || atrValue.IsEmpty || !atrValue.IsFormed) return;
var atr = atrValue.GetValue<decimal>();
if (atr <= 0m) return;
Sides? side = up && low ? Sides.Buy : down && high ? Sides.Sell : null;
if (side is null) return;
if (StopLossAtr > 0m)
StartProtection(new Unit(), new Unit(atr * StopLossAtr, UnitTypes.Absolute), useMarketOrders: true, isLocalStop: true);
Submit(side.Value, Volume, "0341Entry");
}
private void Submit(Sides side, decimal quantity, string comment)
{
_signalOrder = CreateOrder(side, 0m, quantity);
_signalOrder.Type = OrderTypes.Market;
_signalOrder.Comment = comment;
RegisterOrder(_signalOrder);
}
private void LoadObservations()
{
if (string.IsNullOrWhiteSpace(PCRHistoryFile))
throw new InvalidOperationException("PCRHistoryFile is required.");
_observations.Clear();
var lines = File.ReadLines(PCRHistoryFile).GetEnumerator();
using (lines)
{
if (!lines.MoveNext() || lines.Current.Trim() != "SecurityId,Time,Value")
throw new InvalidOperationException("Option observations must begin with SecurityId,Time,Value.");
while (lines.MoveNext())
{
if (string.IsNullOrWhiteSpace(lines.Current)) continue;
var row = lines.Current.Split(',');
if (row.Length != 3) throw new InvalidOperationException("An option observation must have three fields.");
if (!string.Equals(row[0].Trim(), Security.Id, StringComparison.OrdinalIgnoreCase)) continue;
var time = DateTime.ParseExact(row[1].Trim(), "yyyy-MM-dd'T'HH:mm:ss'Z'", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal);
var value = decimal.Parse(row[2], CultureInfo.InvariantCulture);
if (value < 0m || !_observations.TryAdd(time, value))
throw new InvalidOperationException("Option observations must be nonnegative and unique per timestamp.");
}
}
if (_observations.Count == 0) throw new InvalidOperationException("No option observations match the execution security.");
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.BusinessEntities")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import DateTime, Decimal, Math, TimeSpan, InvalidOperationException
from System.Globalization import CultureInfo, DateTimeStyles
from System.IO import File
from StockSharp.Algo.Indicators import SuperTrend, AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
from StockSharp.BusinessEntities import Subscription
from StockSharp.Messages import CandleStates, DataType, Level1Fields, OrderStates, OrderTypes, Sides, Unit, UnitTypes
class supertrend_put_call_ratio_strategy(Strategy):
"""Observed option statistics filter, full signal exits and frozen native ATR protection."""
def __init__(self):
super(supertrend_put_call_ratio_strategy, self).__init__()
self._period = self.Param("Period", 10).SetGreaterThanZero().SetOptimize(5, 20, 3) \
.SetDisplay("Period", "SuperTrend Wilder ATR length", "Parameters")
self._multiplier = self.Param("Multiplier", Decimal(3)).SetGreaterThanZero().SetOptimize(Decimal(2), Decimal(4), Decimal(0.5)) \
.SetDisplay("Multiplier", "SuperTrend band ATR multiplier", "Parameters")
self._pcr_period = self.Param("PCRPeriod", 20).SetRange(2, 100000).SetOptimize(10, 30, 5) \
.SetDisplay("PCRPeriod", "Prior consecutive PCR observations used for the threshold", "Parameters")
self._pcr_multiplier = self.Param("PCRMultiplier", Decimal(2)).SetGreaterThanZero().SetOptimize(Decimal(1), Decimal(3), Decimal(0.5)) \
.SetDisplay("PCRMultiplier", "Sample standard deviation multiplier for PCR extremes", "Parameters")
self._pcr_history_file = self.Param[str]("PCRHistoryFile", None).SetRequired() \
.SetDisplay("PCRHistoryFile", "CSV observed put volume divided by call volume, keyed by candle UTC open time", "Parameters")
self._atr_period = self.Param("AtrPeriod", 14).SetGreaterThanZero() \
.SetDisplay("AtrPeriod", "Wilder ATR length for entry protection", "Parameters")
self._stop_loss_atr = self.Param("StopLossAtr", Decimal(2)).SetRange(Decimal(0), Decimal.MaxValue) \
.SetDisplay("StopLossAtr", "Entry-frozen native stop distance in ATR multiples; zero disables it for a fresh run", "Parameters")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(15))) \
.SetDisplay("CandleType", "Completed signal candle type", "Parameters")
self._observations = {}
self._history = []
self._signal_order = None
@property
def Period(self):
return self._period.Value
@Period.setter
def Period(self, value):
self._period.Value = value
@property
def Multiplier(self):
return self._multiplier.Value
@Multiplier.setter
def Multiplier(self, value):
self._multiplier.Value = value
@property
def PCRPeriod(self):
return self._pcr_period.Value
@PCRPeriod.setter
def PCRPeriod(self, value):
self._pcr_period.Value = value
@property
def PCRMultiplier(self):
return self._pcr_multiplier.Value
@PCRMultiplier.setter
def PCRMultiplier(self, value):
self._pcr_multiplier.Value = value
@property
def PCRHistoryFile(self):
return self._pcr_history_file.Value
@PCRHistoryFile.setter
def PCRHistoryFile(self, value):
self._pcr_history_file.Value = value
@property
def AtrPeriod(self):
return self._atr_period.Value
@AtrPeriod.setter
def AtrPeriod(self, value):
self._atr_period.Value = value
@property
def StopLossAtr(self):
return self._stop_loss_atr.Value
@StopLossAtr.setter
def StopLossAtr(self, value):
self._stop_loss_atr.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def GetWorkingSecurities(self):
return [(self.Security, self.CandleType)]
def OnReseted(self):
super(supertrend_put_call_ratio_strategy, self).OnReseted()
self._observations = {}
self._history = []
self._signal_order = None
def OnStarted2(self, time):
self._load_observations()
super(supertrend_put_call_ratio_strategy, self).OnStarted2(time)
signal = SuperTrend()
signal.Length = self.Period
signal.Multiplier = self.Multiplier
atr = AverageTrueRange()
atr.Length = self.AtrPeriod
subscription = self.SubscribeCandles(self.CandleType)
subscription.BindEx(signal, atr, self._process_candle, True).Start()
for field in (Level1Fields.BestBidPrice, Level1Fields.BestAskPrice):
quote = Subscription(DataType.Level1, self.Security)
quote.MarketData.BuildField = field
self.SubscribeLevel1(quote).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, signal)
self.DrawOwnTrades(area)
def _process_candle(self, candle, signal_value, atr_value):
if candle.State != CandleStates.Finished:
return
ready = candle.OpenTime in self._observations
low = False
high = False
if ready and len(self._history) == self.PCRPeriod:
observation = self._observations[candle.OpenTime]
mean = sum(self._history, Decimal(0)) / Decimal(len(self._history))
variance = sum(((v - mean) * (v - mean) for v in self._history), Decimal(0)) / Decimal(len(self._history) - 1)
deviation = Decimal(Math.Sqrt(float(variance)))
low = observation < mean - self.PCRMultiplier * deviation
high = observation > mean + self.PCRMultiplier * deviation
if not ready:
self._history = []
else:
self._history.append(self._observations[candle.OpenTime])
self._history = self._history[-self.PCRPeriod:]
if signal_value.IsEmpty or not signal_value.IsFormed:
return
current = signal_value.GetValue[Decimal](None)
up = candle.ClosePrice > current
down = candle.ClosePrice < current
if not self.IsFormedAndOnlineAndAllowTrading() or (self._signal_order is not None and self._signal_order.State not in (OrderStates.Done, OrderStates.Failed)):
return
if (self.Position > 0 and down) or (self.Position < 0 and up):
self._submit(Sides.Sell if self.Position > 0 else Sides.Buy, abs(self.Position), "0341Exit")
return
if self.Position != 0 or atr_value.IsEmpty or not atr_value.IsFormed:
return
atr = atr_value.GetValue[Decimal](None)
if atr <= 0:
return
side = Sides.Buy if up and low else Sides.Sell if down and high else None
if side is None:
return
if self.StopLossAtr > 0:
self.StartProtection(Unit(), Unit(atr * self.StopLossAtr, UnitTypes.Absolute), useMarketOrders=True, isLocalStop=True)
self._submit(side, self.Volume, "0341Entry")
def _submit(self, side, quantity, comment):
order = self.CreateOrder(side, Decimal(0), quantity)
order.Type = OrderTypes.Market
order.Comment = comment
self._signal_order = order
self.RegisterOrder(order)
def _load_observations(self):
if not self.PCRHistoryFile:
raise InvalidOperationException("PCRHistoryFile is required.")
self._observations = {}
lines = iter(File.ReadLines(self.PCRHistoryFile))
if next(lines, "").strip() != "SecurityId,Time,Value":
raise InvalidOperationException("Option observations must begin with SecurityId,Time,Value.")
for line in lines:
if not line.strip():
continue
row = line.split(",")
if len(row) != 3:
raise InvalidOperationException("An option observation must have three fields.")
if row[0].strip().lower() != self.Security.Id.lower():
continue
time = DateTime.ParseExact(row[1].strip(), "yyyy-MM-dd'T'HH:mm:ss'Z'", CultureInfo.InvariantCulture,
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal)
value = Decimal.Parse(row[2], CultureInfo.InvariantCulture)
if value < 0 or time in self._observations:
raise InvalidOperationException("Option observations must be nonnegative and unique per timestamp.")
self._observations[time] = value
if not self._observations:
raise InvalidOperationException("No option observations match the execution security.")
def CreateClone(self):
return supertrend_put_call_ratio_strategy()