chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,551 @@
|
||||
/*
|
||||
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
|
||||
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using QuantConnect.Brokerages;
|
||||
using QuantConnect.Data;
|
||||
using QuantConnect.Data.Market;
|
||||
using QuantConnect.Indicators;
|
||||
using QuantConnect.Orders;
|
||||
using QuantConnect.Securities;
|
||||
|
||||
namespace QuantConnect.Algorithm.CSharp
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// QCU: Opening Breakout Algorithm
|
||||
///
|
||||
/// In this algorithm we attempt to provide a working algorithm that
|
||||
/// addresses many of the primary algorithm concerns. These concerns
|
||||
/// are:
|
||||
///
|
||||
/// 1. Signal Generation.
|
||||
/// This algorithm aims to generate signals for an opening
|
||||
/// breakout move before 10am. Signals are generated by
|
||||
/// producing the opening five minute bar, and then trading
|
||||
/// in the direction of the breakout from that bar.
|
||||
///
|
||||
/// 2. Position Sizing.
|
||||
/// Positions are sized using recently the average true range.
|
||||
/// The higher the recently movement, the smaller position.
|
||||
/// This helps to reduce the risk of losing a lot on a single
|
||||
/// transaction.
|
||||
///
|
||||
/// 3. Active Stop Loss.
|
||||
/// Stop losses are maintained at a fixed global percentage to
|
||||
/// limit maximum losses per day, while also a trailing stop
|
||||
/// loss is implemented using the parabolic stop and reverse
|
||||
/// in order to gauge exit points.
|
||||
///
|
||||
/// </summary>
|
||||
/// <meta name="tag" content="strategy example" />
|
||||
/// <meta name="tag" content="indicators" />
|
||||
public class OpeningBreakoutAlgorithm : QCAlgorithm
|
||||
{
|
||||
#pragma warning disable 00162 // File contains unreachable code when EnableOrderUpdateLogging is false
|
||||
|
||||
// the equity symbol we're trading
|
||||
private const string symbol = "SPY";
|
||||
|
||||
// plotting and logging control
|
||||
private const bool EnablePlotting = true;
|
||||
private const bool EnableOrderUpdateLogging = false;
|
||||
private const int PricePlotFrequencyInSeconds = 15;
|
||||
|
||||
// risk control
|
||||
private const decimal MaximumLeverage = 4;
|
||||
private const decimal GlobalStopLossPercent = 0.001m;
|
||||
private const decimal PercentProfitStartPsarTrailingStop = 0.0003m;
|
||||
private const decimal MaximumPorfolioRiskPercentPerPosition = .0025m;
|
||||
|
||||
// entrance criteria
|
||||
private const int OpeningSpanInMinutes = 3;
|
||||
private const decimal BreakoutThresholdPercent = 0.00005m;
|
||||
private const decimal AtrVolatilityThresholdPercent = 0.00275m;
|
||||
private const decimal StdVolatilityThresholdPercent = 0.005m;
|
||||
|
||||
// this is the security we're trading
|
||||
private Security _security;
|
||||
|
||||
// define our indicators used for trading decisions
|
||||
private AverageTrueRange ATR14;
|
||||
private StandardDeviation STD14;
|
||||
private AverageDirectionalIndex ADX14;
|
||||
private ParabolicStopAndReverse PSARMin;
|
||||
|
||||
// smoothed values
|
||||
private ExponentialMovingAverage _smoothedSTD14;
|
||||
private ExponentialMovingAverage _smoothedATR14;
|
||||
|
||||
// working variable to control our algorithm
|
||||
|
||||
// this flag is used to run some code only once after the algorithm is warmed up
|
||||
private bool FinishedWarmup;
|
||||
// this is used to record the last time we closed a position
|
||||
private DateTime LastExitTime;
|
||||
// this is our opening n minute bar
|
||||
private TradeBar OpeningBarRange;
|
||||
// this is the ticket from our market order (entrance)
|
||||
private OrderTicket MarketTicket;
|
||||
// this is the ticket from our stop loss order (exit)
|
||||
private OrderTicket StopLossTicket;
|
||||
// this flag is used to indicate we've switched from a global, non changing
|
||||
// stop loss to a dynamic trailing stop using the PSAR
|
||||
private bool EnablePsarTrailingStop;
|
||||
|
||||
/// <summary>
|
||||
/// Initialise the data and resolution required, as well as the cash and start-end dates for your algorithm. All algorithms must initialized.
|
||||
/// </summary>
|
||||
public override void Initialize()
|
||||
{
|
||||
// initialize algorithm level parameters
|
||||
SetStartDate(2013, 10, 07);
|
||||
SetEndDate(2013, 10, 11);
|
||||
//SetStartDate(2014, 01, 01);
|
||||
//SetEndDate(2014, 06, 01);
|
||||
SetCash(100000);
|
||||
|
||||
// leverage tradier $1 traders
|
||||
SetBrokerageModel(BrokerageName.TradierBrokerage);
|
||||
|
||||
// request high resolution equity data
|
||||
AddSecurity(SecurityType.Equity, symbol, Resolution.Second);
|
||||
|
||||
// save off our security so we can reference it quickly later
|
||||
_security = Securities[symbol];
|
||||
|
||||
// Set our max leverage
|
||||
_security.SetLeverage(MaximumLeverage);
|
||||
|
||||
// define our longer term indicators
|
||||
ADX14 = ADX(symbol, 28, Resolution.Hour);
|
||||
STD14 = STD(symbol, 14, Resolution.Daily);
|
||||
ATR14 = ATR(symbol, 14, resolution: Resolution.Daily);
|
||||
PSARMin = new ParabolicStopAndReverse(symbol, afStart: 0.0001m, afIncrement: 0.0001m);
|
||||
|
||||
// smooth our ATR over a week, we'll use this to determine if recent volatilty warrants entrance
|
||||
var oneWeekInMarketHours = (int)(5*6.5);
|
||||
_smoothedATR14 = new ExponentialMovingAverage("Smoothed_" + ATR14.Name, oneWeekInMarketHours).Of(ATR14);
|
||||
// smooth our STD over a week as well
|
||||
_smoothedSTD14 = new ExponentialMovingAverage("Smoothed_"+STD14.Name, oneWeekInMarketHours).Of(STD14);
|
||||
|
||||
// initialize our charts
|
||||
var chart = new Chart(symbol);
|
||||
chart.AddSeries(new Series(ADX14.Name, SeriesType.Line, 0));
|
||||
chart.AddSeries(new Series("Enter", SeriesType.Scatter, 0));
|
||||
chart.AddSeries(new Series("Exit", SeriesType.Scatter, 0));
|
||||
chart.AddSeries(new Series(PSARMin.Name, SeriesType.Scatter, 0));
|
||||
AddChart(chart);
|
||||
|
||||
var history = History(symbol, 20, Resolution.Daily);
|
||||
foreach (var bar in history)
|
||||
{
|
||||
ADX14.Update(bar);
|
||||
ATR14.Update(bar);
|
||||
STD14.Update(bar.EndTime, bar.Close);
|
||||
}
|
||||
|
||||
// schedule an event to run every day at five minutes after our symbol's market open
|
||||
Schedule.Event("MarketOpenSpan")
|
||||
.EveryDay(symbol)
|
||||
.AfterMarketOpen(symbol, minutesAfterOpen: OpeningSpanInMinutes)
|
||||
.Run(MarketOpeningSpanHandler);
|
||||
|
||||
Schedule.Event("MarketOpen")
|
||||
.EveryDay(symbol)
|
||||
.AfterMarketOpen(symbol, minutesAfterOpen: -1)
|
||||
.Run(() => PSARMin.Reset());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This function is scheduled to be run every day at the specified number of minutes after market open
|
||||
/// </summary>
|
||||
public void MarketOpeningSpanHandler()
|
||||
{
|
||||
// request the last n minutes of data in minute bars, we're going to
|
||||
// define the opening rang
|
||||
var history = History(symbol, OpeningSpanInMinutes, Resolution.Minute);
|
||||
|
||||
// this is our bar size
|
||||
var openingSpan = TimeSpan.FromMinutes(OpeningSpanInMinutes);
|
||||
|
||||
// we only care about the high and low here
|
||||
OpeningBarRange = new TradeBar
|
||||
{
|
||||
// time values
|
||||
Time = Time - openingSpan,
|
||||
EndTime = Time,
|
||||
Period = openingSpan,
|
||||
// high and low
|
||||
High = _security.Close,
|
||||
Low = _security.Close
|
||||
};
|
||||
|
||||
// aggregate the high/low for the opening range
|
||||
foreach (var tradeBar in history)
|
||||
{
|
||||
OpeningBarRange.Low = Math.Min(OpeningBarRange.Low, tradeBar.Low);
|
||||
OpeningBarRange.High = Math.Max(OpeningBarRange.High, tradeBar.High);
|
||||
}
|
||||
|
||||
// widen the bar when looking for breakouts
|
||||
OpeningBarRange.Low *= 1 - BreakoutThresholdPercent;
|
||||
OpeningBarRange.High *= 1 + BreakoutThresholdPercent;
|
||||
|
||||
Log("---------" + Time.Date + "---------");
|
||||
Log("OpeningBarRange: Low: " + OpeningBarRange.Low.SmartRounding() + " High: " + OpeningBarRange.High.SmartRounding());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// OnData event is the primary entry point for your algorithm. Each new data point will be pumped in here.
|
||||
/// </summary>
|
||||
/// <param name="data">Slice object keyed by symbol containing the stock data</param>
|
||||
public override void OnData(Slice data)
|
||||
{
|
||||
// we don't need to run any of this during our warmup phase
|
||||
if (IsWarmingUp) return;
|
||||
|
||||
// when we're done warming up, register our indicators to start plotting
|
||||
if (!IsWarmingUp && !FinishedWarmup)
|
||||
{
|
||||
// this is a run once flag for when we're finished warmup
|
||||
FinishedWarmup = true;
|
||||
|
||||
// plot our hourly indicators automatically, wait for them to ready
|
||||
PlotIndicator("ADX", ADX14);
|
||||
PlotIndicator("ADX", ADX14.NegativeDirectionalIndex, ADX14.PositiveDirectionalIndex);
|
||||
|
||||
PlotIndicator("ATR", true, ATR14);
|
||||
PlotIndicator("STD", true, STD14);
|
||||
PlotIndicator("ATR", true, _smoothedATR14);
|
||||
}
|
||||
|
||||
// update our PSAR
|
||||
PSARMin.Update((TradeBar) _security.GetLastData());
|
||||
|
||||
// plot price until an hour after we close so we can see our execution skillz
|
||||
if (ShouldPlot)
|
||||
{
|
||||
// we can plot price more often if we want
|
||||
Plot(symbol, "Price", _security.Close);
|
||||
// only plot psar on the minute
|
||||
if (PSARMin.IsReady)
|
||||
{
|
||||
Plot(symbol, PSARMin);
|
||||
}
|
||||
}
|
||||
|
||||
// first wait for our opening range bar to be set to today
|
||||
if (OpeningBarRange == null || OpeningBarRange.EndTime.Date != Time.Date || OpeningBarRange.EndTime == Time) return;
|
||||
|
||||
// we only trade max once per day, so if we've already exited the stop loss, bail
|
||||
if (StopLossTicket != null && StopLossTicket.Status == OrderStatus.Filled)
|
||||
{
|
||||
// null these out to signal that we're done trading for the day
|
||||
OpeningBarRange = null;
|
||||
StopLossTicket = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// now that we have our opening bar, test to see if we're already in a position
|
||||
if (!_security.Invested)
|
||||
{
|
||||
ScanForEntrance();
|
||||
}
|
||||
else
|
||||
{
|
||||
// if we haven't exited yet then manage our stop loss, this controls our exit point
|
||||
if (_security.Invested)
|
||||
{
|
||||
ManageStopLoss();
|
||||
}
|
||||
else if (StopLossTicket != null && StopLossTicket.Status.IsOpen())
|
||||
{
|
||||
StopLossTicket.Cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scans for a breakout from the opening range bar
|
||||
/// </summary>
|
||||
private void ScanForEntrance()
|
||||
{
|
||||
// scan for entrances, we only want to do this before 10am
|
||||
if (Time.TimeOfDay.Hours >= 10) return;
|
||||
|
||||
// expect capture 10% of the daily range
|
||||
var expectedCaptureRange = 0.1m*ATR14;
|
||||
|
||||
var allowedDollarLoss = MaximumPorfolioRiskPercentPerPosition * Portfolio.TotalPortfolioValue;
|
||||
|
||||
var shares = (int) (allowedDollarLoss/expectedCaptureRange);
|
||||
|
||||
// determine a position size based on an acceptable loss in proporton to our total portfolio value
|
||||
//var shares = (int) (MaximumLeverage*MaximumPorfolioRiskPercentPerPosition*Portfolio.TotalPortfolioValue/(0.4m*ATR14));
|
||||
|
||||
// max out at a little below our stated max, prevents margin calls and such
|
||||
var maxShare = (int) CalculateOrderQuantity(symbol, MaximumLeverage);
|
||||
shares = Math.Min(shares, maxShare);
|
||||
|
||||
// min out at 1x leverage
|
||||
//var minShare = CalculateOrderQuantity(symbol, MaximumLeverage/2m);
|
||||
//shares = Math.Max(shares, minShare);
|
||||
|
||||
// we're looking for a breakout of the opening range bar in the direction of the medium term trend
|
||||
if (ShouldEnterLong)
|
||||
{
|
||||
// breakout to the upside, go long (fills synchronously)
|
||||
MarketTicket = MarketOrder(symbol, shares);
|
||||
Log("Enter long @ " + MarketTicket.AverageFillPrice.SmartRounding() + " Shares: " + shares);
|
||||
Plot(symbol, "Enter", MarketTicket.AverageFillPrice);
|
||||
|
||||
// we'll start with a global, non-trailing stop loss
|
||||
EnablePsarTrailingStop = false;
|
||||
|
||||
// submit stop loss order for max loss on the trade
|
||||
var stopPrice = _security.Low*(1 - GlobalStopLossPercent);
|
||||
StopLossTicket = StopMarketOrder(symbol, -shares, stopPrice);
|
||||
if (EnableOrderUpdateLogging)
|
||||
{
|
||||
Log("Submitted stop loss @ " + stopPrice.SmartRounding());
|
||||
}
|
||||
}
|
||||
else if (ShouldEnterShort)
|
||||
{
|
||||
// breakout to the downside, go short
|
||||
MarketTicket = MarketOrder(symbol, - -shares);
|
||||
Log("Enter short @ " + MarketTicket.AverageFillPrice.SmartRounding());
|
||||
Plot(symbol, "Enter", MarketTicket.AverageFillPrice);
|
||||
|
||||
// we'll start with a global, non-trailing stop loss
|
||||
EnablePsarTrailingStop = false;
|
||||
|
||||
// submit stop loss order for max loss on the trade
|
||||
var stopPrice = _security.High*(1 + GlobalStopLossPercent);
|
||||
StopLossTicket = StopMarketOrder(symbol, -shares, stopPrice);
|
||||
if (EnableOrderUpdateLogging)
|
||||
{
|
||||
Log("Submitted stop loss @ " + stopPrice.SmartRounding() + " Shares: " + shares);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Manages our stop loss ticket
|
||||
/// </summary>
|
||||
private void ManageStopLoss()
|
||||
{
|
||||
// if we've already exited then no need to do more
|
||||
if (StopLossTicket == null || StopLossTicket.Status == OrderStatus.Filled) return;
|
||||
|
||||
// only do this once per minute
|
||||
//if (Time.RoundDown(TimeSpan.FromMinutes(1)) != Time) return;
|
||||
|
||||
// get the current stop price
|
||||
var stopPrice = StopLossTicket.Get(OrderField.StopPrice);
|
||||
|
||||
// check for enabling the psar trailing stop
|
||||
if (ShouldEnablePsarTrailingStop(stopPrice))
|
||||
{
|
||||
EnablePsarTrailingStop = true;
|
||||
Log("Enabled PSAR trailing stop @ ProfitPercent: " + _security.Holdings.UnrealizedProfitPercent.SmartRounding());
|
||||
}
|
||||
|
||||
// we've trigger the psar trailing stop, so start updating our stop loss tick
|
||||
if (EnablePsarTrailingStop && PSARMin.IsReady)
|
||||
{
|
||||
StopLossTicket.Update(new UpdateOrderFields {StopPrice = PSARMin});
|
||||
Log("Submitted stop loss @ " + PSARMin.Current.Value.SmartRounding());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This event handler is fired for each and every order event the algorithm
|
||||
/// receives. We'll perform some logging and house keeping here
|
||||
/// </summary>
|
||||
public override void OnOrderEvent(OrderEvent orderEvent)
|
||||
{
|
||||
// print debug messages for all order events
|
||||
if (LiveMode || orderEvent.Status.IsFill() || EnableOrderUpdateLogging)
|
||||
{
|
||||
LiveDebug("Filled: " + orderEvent.FillQuantity + " Price: " + orderEvent.FillPrice);
|
||||
}
|
||||
|
||||
// if this is a fill and we now don't own any stock, that means we've closed for the day
|
||||
if (!_security.Invested && orderEvent.Status == OrderStatus.Filled)
|
||||
{
|
||||
// reset values for tomorrow
|
||||
LastExitTime = Time;
|
||||
var ticket = Transactions.GetOrderTickets(x => x.OrderId == orderEvent.OrderId).Single();
|
||||
Plot(symbol, "Exit", ticket.AverageFillPrice);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If we're still invested by the end of the day, liquidate
|
||||
/// </summary>
|
||||
public override void OnEndOfDay(Symbol symbol)
|
||||
{
|
||||
if (symbol == _security.Symbol && _security.Invested)
|
||||
{
|
||||
Liquidate();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether or not we should plot. This is used
|
||||
/// to provide enough plot points but not too many, we don't
|
||||
/// need to plot every second in backtests to get an idea of
|
||||
/// how good or bad our algorithm is performing
|
||||
/// </summary>
|
||||
public bool ShouldPlot
|
||||
{
|
||||
get
|
||||
{
|
||||
// always in live
|
||||
if (LiveMode) return true;
|
||||
// set in top to override plotting during long backtests
|
||||
if (!EnablePlotting) return false;
|
||||
// every 30 seconds in backtest
|
||||
if (Time.RoundDown(TimeSpan.FromSeconds(PricePlotFrequencyInSeconds)) != Time) return false;
|
||||
// always if we're invested
|
||||
if (_security.Invested) return true;
|
||||
// always if it's before noon
|
||||
if (Time.TimeOfDay.Hours < 10.25) return true;
|
||||
// for an hour after our exit
|
||||
if (Time - LastExitTime < TimeSpan.FromMinutes(30)) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// In live mode it's nice to push messages to the debug window
|
||||
/// as well as the log, this allows easy real time inspection of
|
||||
/// how the algorithm is performing
|
||||
/// </summary>
|
||||
public void LiveDebug(object msg)
|
||||
{
|
||||
if (msg == null) return;
|
||||
|
||||
if (LiveMode)
|
||||
{
|
||||
Debug(msg.ToString());
|
||||
Log(msg.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
Log(msg.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether or not we should end a long position
|
||||
/// </summary>
|
||||
private bool ShouldEnterLong
|
||||
{
|
||||
// check to go in the same direction of longer term trend and opening break out
|
||||
get
|
||||
{
|
||||
return IsUptrend
|
||||
&& HasEnoughRecentVolatility
|
||||
&& _security.Close > OpeningBarRange.High;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether or not we're currently in a medium term up trend
|
||||
/// </summary>
|
||||
private bool IsUptrend
|
||||
{
|
||||
get { return ADX14 > 20 && ADX14.PositiveDirectionalIndex > ADX14.NegativeDirectionalIndex; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether or not we should enter a short position
|
||||
/// </summary>
|
||||
private bool ShouldEnterShort
|
||||
{
|
||||
// check to go in the same direction of longer term trend and opening break out
|
||||
get
|
||||
{
|
||||
return IsDowntrend
|
||||
&& HasEnoughRecentVolatility
|
||||
&& _security.Close < OpeningBarRange.Low;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether or not we're currently in a medium term down trend
|
||||
/// </summary>
|
||||
private bool IsDowntrend
|
||||
{
|
||||
get { return ADX14 > 20 && ADX14.NegativeDirectionalIndex > ADX14.PositiveDirectionalIndex; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether or not there's been enough recent volatility for
|
||||
/// this strategy to work
|
||||
/// </summary>
|
||||
private bool HasEnoughRecentVolatility
|
||||
{
|
||||
get
|
||||
{
|
||||
return _smoothedATR14 > _security.Close*AtrVolatilityThresholdPercent
|
||||
|| _smoothedSTD14 > _security.Close*StdVolatilityThresholdPercent;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether or not we should enable the psar trailing stop
|
||||
/// </summary>
|
||||
/// <param name="stopPrice">current stop price of our stop loss tick</param>
|
||||
private bool ShouldEnablePsarTrailingStop(decimal stopPrice)
|
||||
{
|
||||
// no need to enable if it's already enabled
|
||||
return !EnablePsarTrailingStop
|
||||
// once we're up a certain percentage, we'll use PSAR to control our stop
|
||||
&& _security.Holdings.UnrealizedProfitPercent > PercentProfitStartPsarTrailingStop
|
||||
// make sure the PSAR is on the right side
|
||||
&& PsarIsOnRightSideOfPrice
|
||||
// make sure the PSAR is more profitable than our global loss
|
||||
&& IsPsarMoreProfitableThanStop(stopPrice);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether or not the PSAR is on the right side of price depending on our long/short
|
||||
/// </summary>
|
||||
private bool PsarIsOnRightSideOfPrice
|
||||
{
|
||||
get
|
||||
{
|
||||
return (_security.Holdings.IsLong && PSARMin < _security.Close)
|
||||
|| (_security.Holdings.IsShort && PSARMin > _security.Close);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether or not the PSAR stop price is better than the specified stop price
|
||||
/// </summary>
|
||||
private bool IsPsarMoreProfitableThanStop(decimal stopPrice)
|
||||
{
|
||||
return (_security.Holdings.IsLong && PSARMin > stopPrice)
|
||||
|| (_security.Holdings.IsShort && PSARMin < stopPrice);
|
||||
}
|
||||
#pragma warning restore 00162
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user