27de93f78f
Syntax Tests / build (push) Has been cancelled
API Tests / build (push) Has been cancelled
Benchmarks / build (push) Has been cancelled
Build & Test Lean / build (push) Has been cancelled
Regression Tests / build (push) Has been cancelled
Report Generator Tests / build (push) Has been cancelled
Research Regression Tests / build (push) Has been cancelled
Python Virtual Environments / build (push) Has been cancelled
* Market orders wait for fresh data instead of filling on stale prices A market order would previously fill immediately on the most recent available data even when that data was older than StalePriceTimeSpan (default one hour), only attaching a warning. This is unrealistic for a coarse resolution asset (hour/daily) where the latest bar is the stale previous close when the order is placed mid-bar or via an intraday scheduled event. The default fill models (FillModel, EquityFillModel, FutureFillModel) now wait for fresh data instead of filling on a stale price, but only for hour and daily resolutions; the order fills when the next bar closes. For minute/second/tick subscriptions the previous behavior is kept (fill on the stale price with a warning), since stale data there is a genuine gap rather than a bar still forming. Adds HourResolutionMarketOrderStalePriceRegressionAlgorithm, updates the FillOutsideHours daily expectation, and regenerates statistics for the hour/daily algorithms whose fills change. FutureOptionDaily buys and liquidates a day apart now (a same-day buy + liquidate cannot fill on daily data once stale fills are disabled). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Normalize and correct StalePriceTimeSpan XML docs The interface and class docs now match and reflect the actual behavior: the wait-for-fresh-data only applies to hour/daily resolutions, while minute/second/tick subscriptions still fill on stale data with a warning. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Fill resting market orders at the bar open instead of the close A hour/daily market order that was resting before the current bar opened (it predates the bar - placed after the previous close or while waiting for fresh data) now fills at the bar open, the price when trading resumed (like a MarketOnOpen), instead of the bar close. Orders placed during the bar still fill at the current/close price, so intraday mid-bar fills are unchanged. Equity fills are unchanged (resting equity orders are already converted to MarketOnOpen by QCAlgorithm.MarketOrder). Adds the shared FillModel.GetMarketFillPrice helper used by the base FillModel and FutureFillModel, a unit test, and regenerates statistics for the affected daily/hour futures, index and crypto regression algorithms (order counts unchanged, only fill prices). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Add regression algorithm asserting resting market orders fill at the bar open RestingMarketOrderFillsAtBarOpenRegressionAlgorithm buys a daily future on the bar that delivers it (fills at that bar's close) and submits a liquidation while the market is closed (overnight pulse, no fresh bar). The liquidation rests and fills on a later bar at the bar open, not its close - asserting the new GetMarketFillPrice behavior. The in-bar buy is asserted to fill at the close, for contrast. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Carry the bar start time on Prices instead of re-reading the cache Add Prices.Time (the bar start, mirroring BaseData.Time/EndTime), populated from the source bar/tick in every GetPrices path. GetMarketFillPrice now uses prices.Time directly instead of a second asset.Cache.GetData() lookup. Behavior is unchanged (prices.Time equals the previously read cache time). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Add regression algorithm asserting in-session hour orders fill at the latest close HourMarketOrderFillsAtBarCloseRegressionAlgorithm submits an hour resolution market order mid-bar (via an intraday scheduled event) while the market is open, using the default one hour StalePriceTimeSpan. It asserts the order fills immediately at the latest available bar's close - not waiting and not at the bar open - since the latest bar is within the stale window. Guards the resting-order open-fill behavior against affecting ordinary in-session fills. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Regenerate custom fill model algorithm statistics for the open-fill change CustomModelsAlgorithm and CustomPartialFillModelAlgorithm subscribe SPY at hour resolution and their custom fill models delegate to base.MarketFill, so resting orders now fill at the bar open. Regenerate their statistics (C#/Python) and the inline expected statistics of the PEP8StyleCustomModelsWork test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
392 lines
14 KiB
C#
392 lines
14 KiB
C#
/*
|
|
* 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 NUnit.Framework;
|
|
using Python.Runtime;
|
|
using QuantConnect.Python;
|
|
using System.Collections.Generic;
|
|
using QuantConnect.Statistics;
|
|
using System.Reflection;
|
|
|
|
namespace QuantConnect.Tests.Python
|
|
{
|
|
public static class PythonWrapperTests
|
|
{
|
|
[TestFixture]
|
|
public class ValidateImplementationOf
|
|
{
|
|
[TestCase(nameof(MissingMethodOne), "ModelMissingMethodOne", "MethodOne")]
|
|
[TestCase(nameof(MissingProperty), "ModelMissingProperty", "PropertyOne")]
|
|
public void ThrowsOnMissingMember(string moduleName, string className, string missingMemberName)
|
|
{
|
|
using (Py.GIL())
|
|
{
|
|
var moduleStr = GetFieldValue(moduleName);
|
|
var module = PyModule.FromString(nameof(ValidateImplementationOf), moduleStr);
|
|
var model = module.GetAttr(className).Invoke();
|
|
Assert.That(() => model.ValidateImplementationOf<IModel>(), Throws
|
|
.Exception.InstanceOf<NotImplementedException>().With.Message.Contains(missingMemberName));
|
|
}
|
|
}
|
|
|
|
[TestCase(nameof(FullyImplemented), "FullyImplementedModel")]
|
|
[TestCase(nameof(FullyImplementedSnakeCase), "FullyImplementedSnakeCaseModel")]
|
|
[TestCase(nameof(FullyImplementedWithPropertyAsField), "FullyImplementedModelWithPropertyAsField")]
|
|
[TestCase(nameof(DerivedFromCsharp), "DerivedFromCSharpModel")]
|
|
public void DoesNotThrowWhenInterfaceFullyImplemented(string moduleName, string className)
|
|
{
|
|
using (Py.GIL())
|
|
{
|
|
var moduleStr = GetFieldValue(moduleName);
|
|
var module = PyModule.FromString(nameof(ValidateImplementationOf), moduleStr);
|
|
var model = module.GetAttr(className).Invoke();
|
|
Assert.That(() => model.ValidateImplementationOf<IModel>(), Throws.Nothing);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void SettlementModelPythonWrapperWorks()
|
|
{
|
|
var results = AlgorithmRunner.RunLocalBacktest("CustomSettlementModelRegressionAlgorithm",
|
|
new Dictionary<string, string>()
|
|
{
|
|
{PerformanceMetrics.TotalOrders, "0"},
|
|
{"Average Win", "0%"},
|
|
{"Average Loss", "0%"},
|
|
{"Compounding Annual Return", "119.460%"},
|
|
{"Drawdown", "0%"},
|
|
{"Expectancy", "0"},
|
|
{"Net Profit", "1.010%"},
|
|
{"Sharpe Ratio", "-5.989"},
|
|
{"Sortino Ratio", "0"},
|
|
{"Probabilistic Sharpe Ratio", "1.216%"},
|
|
{"Loss Rate", "0%"},
|
|
{"Win Rate", "0%"},
|
|
{"Profit-Loss Ratio", "0"},
|
|
{"Alpha", "-0.411"},
|
|
{"Beta", "-0.033"},
|
|
{"Annual Standard Deviation", "0.079"},
|
|
{"Annual Variance", "0.006"},
|
|
{"Information Ratio", "-10.086"},
|
|
{"Tracking Error", "0.243"},
|
|
{"Treynor Ratio", "14.619"},
|
|
{"Total Fees", "$0.00"},
|
|
{"Estimated Strategy Capacity", "$0"},
|
|
{"Lowest Capacity Asset", ""},
|
|
{"Portfolio Turnover", "0%"},
|
|
{"OrderListHash", "d41d8cd98f00b204e9800998ecf8427e"}
|
|
},
|
|
Language.Python,
|
|
AlgorithmStatus.Completed,
|
|
algorithmLocation: "../../../Algorithm.Python/CustomSettlementModelRegressionAlgorithm.py"
|
|
);
|
|
}
|
|
|
|
[Test]
|
|
public void BenchmarkModelPythonWrapperWorks()
|
|
{
|
|
var results = AlgorithmRunner.RunLocalBacktest("CustomBenchmarkRegressionAlgorithm",
|
|
new Dictionary<string, string>()
|
|
{
|
|
{PerformanceMetrics.TotalOrders, "0"},
|
|
{"Average Win", "0%"},
|
|
{"Average Loss", "0%"},
|
|
{"Compounding Annual Return", "0%"},
|
|
{"Drawdown", "0%"},
|
|
{"Expectancy", "0"},
|
|
{"Net Profit", "0%"},
|
|
{"Sharpe Ratio", "0"},
|
|
{"Sortino Ratio", "0"},
|
|
{"Probabilistic Sharpe Ratio", "0%"},
|
|
{"Loss Rate", "0%"},
|
|
{"Win Rate", "0%"},
|
|
{"Profit-Loss Ratio", "0"},
|
|
{"Alpha", "0"},
|
|
{"Beta", "0"},
|
|
{"Annual Standard Deviation", "0"},
|
|
{"Annual Variance", "0"},
|
|
{"Information Ratio", "-6.654540153820717E+27"},
|
|
{"Tracking Error", "11.906"},
|
|
{"Treynor Ratio", "0"},
|
|
{"Total Fees", "$0.00"},
|
|
{"Estimated Strategy Capacity", "$0"},
|
|
{"Lowest Capacity Asset", ""},
|
|
{"Portfolio Turnover", "0%"},
|
|
{"OrderListHash", "d41d8cd98f00b204e9800998ecf8427e"}
|
|
},
|
|
Language.Python,
|
|
AlgorithmStatus.Completed,
|
|
algorithmLocation: "../../../Algorithm.Python/CustomBenchmarkRegressionAlgorithm.py"
|
|
);
|
|
}
|
|
|
|
[Test]
|
|
public void PEP8StyleAlgorithmsImplementationsWork()
|
|
{
|
|
AlgorithmRunner.RunLocalBacktest("PEP8StyleBasicAlgorithm",
|
|
new Dictionary<string, string>()
|
|
{
|
|
{"Total Orders", "1"},
|
|
{"Average Win", "0%"},
|
|
{"Average Loss", "0%"},
|
|
{"Compounding Annual Return", "271.453%"},
|
|
{"Drawdown", "2.200%"},
|
|
{"Expectancy", "0"},
|
|
{"Start Equity", "100000"},
|
|
{"End Equity", "101691.92"},
|
|
{"Net Profit", "1.692%"},
|
|
{"Sharpe Ratio", "8.854"},
|
|
{"Sortino Ratio", "0"},
|
|
{"Probabilistic Sharpe Ratio", "67.609%"},
|
|
{"Loss Rate", "0%"},
|
|
{"Win Rate", "0%"},
|
|
{"Profit-Loss Ratio", "0"},
|
|
{"Alpha", "-0.005"},
|
|
{"Beta", "0.996"},
|
|
{"Annual Standard Deviation", "0.222"},
|
|
{"Annual Variance", "0.049"},
|
|
{"Information Ratio", "-14.565"},
|
|
{"Tracking Error", "0.001"},
|
|
{"Treynor Ratio", "1.97"},
|
|
{"Total Fees", "$3.44"},
|
|
{"Estimated Strategy Capacity", "$56000000.00"},
|
|
{"Lowest Capacity Asset", "SPY R735QTJ8XC9X"},
|
|
{"Portfolio Turnover", "19.93%"},
|
|
{"OrderListHash", "3da9fa60bf95b9ed148b95e02e0cfc9e"}
|
|
},
|
|
Language.Python,
|
|
AlgorithmStatus.Completed,
|
|
algorithmLocation: "../../../Algorithm.Python/PEP8StyleBasicAlgorithm.py"
|
|
);
|
|
}
|
|
|
|
[Test]
|
|
public void PEP8StyleCustomModelsWork()
|
|
{
|
|
AlgorithmRunner.RunLocalBacktest("CustomModelsPEP8Algorithm",
|
|
new Dictionary<string, string>()
|
|
{
|
|
{"Total Orders", "63"},
|
|
{"Average Win", "0.10%"},
|
|
{"Average Loss", "-0.06%"},
|
|
{"Compounding Annual Return", "-7.101%"},
|
|
{"Drawdown", "2.400%"},
|
|
{"Expectancy", "-0.181"},
|
|
{"Start Equity", "100000"},
|
|
{"End Equity", "99383.07"},
|
|
{"Net Profit", "-0.617%"},
|
|
{"Sharpe Ratio", "-1.441"},
|
|
{"Sortino Ratio", "-1.977"},
|
|
{"Probabilistic Sharpe Ratio", "22.128%"},
|
|
{"Loss Rate", "69%"},
|
|
{"Win Rate", "31%"},
|
|
{"Profit-Loss Ratio", "1.64"},
|
|
{"Alpha", "-0.101"},
|
|
{"Beta", "0.121"},
|
|
{"Annual Standard Deviation", "0.04"},
|
|
{"Annual Variance", "0.002"},
|
|
{"Information Ratio", "-4.109"},
|
|
{"Tracking Error", "0.102"},
|
|
{"Treynor Ratio", "-0.475"},
|
|
{"Total Fees", "$62.23"},
|
|
{"Estimated Strategy Capacity", "$52000000.00"},
|
|
{"Lowest Capacity Asset", "SPY R735QTJ8XC9X"},
|
|
{"Portfolio Turnover", "197.93%"},
|
|
{"OrderListHash", "fe01fe4923e8856fe3376ece636b4e23"}
|
|
},
|
|
Language.Python,
|
|
AlgorithmStatus.Completed,
|
|
algorithmLocation: "../../../Algorithm.Python/CustomModelsPEP8Algorithm.py"
|
|
);
|
|
}
|
|
|
|
private static string GetFieldValue(string name)
|
|
{
|
|
return typeof(ValidateImplementationOf).GetField(name, BindingFlags.Static | BindingFlags.NonPublic).GetValue(null) as string;
|
|
}
|
|
|
|
private const string FullyImplemented =
|
|
@"
|
|
from clr import AddReference
|
|
AddReference('QuantConnect.Tests')
|
|
|
|
from QuantConnect.Tests.Python import *
|
|
|
|
class FullyImplementedModel:
|
|
def MethodOne():
|
|
pass
|
|
def MethodTwo():
|
|
pass
|
|
@property
|
|
def PropertyOne(self):
|
|
return 'value'
|
|
|
|
";
|
|
|
|
private const string FullyImplementedSnakeCase =
|
|
@"
|
|
from clr import AddReference
|
|
AddReference('QuantConnect.Tests')
|
|
|
|
from QuantConnect.Tests.Python import *
|
|
|
|
class FullyImplementedSnakeCaseModel:
|
|
def method_one():
|
|
pass
|
|
def method_two():
|
|
pass
|
|
@property
|
|
def property_one(self):
|
|
pass
|
|
|
|
";
|
|
|
|
private const string FullyImplementedWithPropertyAsField =
|
|
@"
|
|
from clr import AddReference
|
|
AddReference('QuantConnect.Tests')
|
|
|
|
from QuantConnect.Tests.Python import *
|
|
|
|
class FullyImplementedModelWithPropertyAsField:
|
|
def method_one():
|
|
pass
|
|
def method_two():
|
|
pass
|
|
def __init__(self):
|
|
self.property_one = 'value'
|
|
";
|
|
|
|
private const string DerivedFromCsharp =
|
|
@"
|
|
from clr import AddReference
|
|
AddReference('QuantConnect.Tests')
|
|
|
|
from QuantConnect.Tests.Python import *
|
|
|
|
class DerivedFromCSharpModel(PythonWrapperTests.ValidateImplementationOf.Model):
|
|
def MethodOne():
|
|
pass
|
|
@property
|
|
def PropertyOne(self):
|
|
return 'value'
|
|
|
|
";
|
|
|
|
private const string MissingMethodOne =
|
|
@"
|
|
from clr import AddReference
|
|
AddReference('QuantConnect.Tests')
|
|
|
|
from QuantConnect.Tests.Python import *
|
|
|
|
class ModelMissingMethodOne:
|
|
def MethodTwo():
|
|
pass
|
|
@property
|
|
def PropertyOne(self):
|
|
return 'value'
|
|
";
|
|
|
|
private const string MissingProperty =
|
|
@"
|
|
from clr import AddReference
|
|
AddReference('QuantConnect.Tests')
|
|
|
|
from QuantConnect.Tests.Python import *
|
|
|
|
class ModelMissingProperty:
|
|
def MethodOne():
|
|
pass
|
|
def MethodTwo():
|
|
pass
|
|
";
|
|
|
|
interface IModel
|
|
{
|
|
string PropertyOne { get; set; }
|
|
void MethodOne();
|
|
void MethodTwo();
|
|
}
|
|
|
|
public class Model : IModel
|
|
{
|
|
public string PropertyOne { get; set; }
|
|
|
|
public void MethodOne()
|
|
{
|
|
}
|
|
|
|
public void MethodTwo()
|
|
{
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
[TestFixture]
|
|
public class InvokeTests
|
|
{
|
|
[Test]
|
|
public void InvokesCSharpMethod()
|
|
{
|
|
using (Py.GIL())
|
|
{
|
|
var module = PyModule.FromString(nameof(InvokeTests), InvokeModule);
|
|
var model = module.GetAttr("PythonInvokeTestsModel").Invoke();
|
|
Assert.That(model.InvokeMethod<int>("AddThreeNumbers", 1, 2, 3), Is.EqualTo(6));
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void InvokesPythonMethod()
|
|
{
|
|
using (Py.GIL())
|
|
{
|
|
var module = PyModule.FromString(nameof(InvokeTests), InvokeModule);
|
|
var model = module.GetAttr("PythonInvokeTestsModel").Invoke();
|
|
Assert.That(model.InvokeMethod<int>("AddTwoNumbers", 1, 2), Is.EqualTo(3));
|
|
}
|
|
}
|
|
|
|
private const string InvokeModule =
|
|
@"
|
|
from clr import AddReference
|
|
AddReference('QuantConnect.Tests')
|
|
|
|
from QuantConnect.Tests.Python import *
|
|
|
|
class PythonInvokeTestsModel(PythonWrapperTests.InvokeTests.InvokeTestsModel):
|
|
def add_two_numbers(self, a, b):
|
|
return a + b
|
|
";
|
|
|
|
public class InvokeTestsModel
|
|
{
|
|
public int AddTwoNumbers(int a, int b)
|
|
{
|
|
throw new NotImplementedException();
|
|
}
|
|
|
|
public int AddThreeNumbers(int a, int b, int c)
|
|
{
|
|
return a + b + c;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|