Skip to content

Commit d407307

Browse files
StefanoRaggimchandschuh
authored andcommitted
Add IObjectStore interface with LocalObjectStore implementation
This commit is squashed from iterative development: - More consistent method naming - Storage root path updated to be absolute and include algorithm name - Storage root path created only if object store is actually used - Implemented XML save/load - Added missing unit tests - Replaced Log.Trace with Log.Error calls - Added the object store name logging in Engine.Main - Read storage root from config - Create algorithm storage root folder in Initialize - Remove empty folder in Dispose - Added null checks in all methods - Added missing XML parameter docs - make Initialize and Dispose virtual - make AlgorithmStorageRoot protected The IObjectStore abstraction provides algorithms with a persistent storage mechanism. While the algorithm is running, data is maintained in memory as a dictionary of raw bytes (string -> byte[]). This ensures we avoid any reference type shenanigans. Periodically, the data in the object store is persisted and additionally, when the algorithm shuts down, the object store's data will again be persisted. This ensures that when the algorithm starts up again, it will have access to any state that has been saved into the object store. A great use case for IObjectStore is saving a compute heavy model. For example, computing the weights of a deep neural network is very CPU intensive, but after the weights are computed, evaluation is fairly quick. An initial backtest can be used to solved for the network's weights and then subsequent backtests or even in live mode, the weights will be available to the algorithm provided they were saved into the object store. Also, some libraries require a file path to load model data. The object store provides a `GetFilePath(key)` method which will copy the data for the provided key to the disk and return that path so the library can load the model data.
1 parent 8af7297 commit d407307

16 files changed

Lines changed: 600 additions & 8 deletions

Algorithm/QCAlgorithm.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,11 @@ public ConcurrentQueue<string> ErrorMessages
520520
/// </summary>
521521
public Slice CurrentSlice { get; private set; }
522522

523+
/// <summary>
524+
/// Gets the object store, used for persistence
525+
/// </summary>
526+
public IObjectStore ObjectStore { get; private set; }
527+
523528
/// <summary>
524529
/// Initialise the data and resolution required, as well as the cash and start-end dates for your algorithm. All algorithms must initialized.
525530
/// </summary>
@@ -2189,6 +2194,15 @@ public void SetApi(IApi api)
21892194
_api = api;
21902195
}
21912196

2197+
/// <summary>
2198+
/// Sets the object store
2199+
/// </summary>
2200+
/// <param name="objectStore">The object store</param>
2201+
public void SetObjectStore(IObjectStore objectStore)
2202+
{
2203+
ObjectStore = objectStore;
2204+
}
2205+
21922206
/// <summary>
21932207
/// Sets the order event provider
21942208
/// </summary>

AlgorithmFactory/Python/Wrappers/AlgorithmPythonWrapper.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,11 @@ public Exception RunTimeError
277277
/// </summary>
278278
public IFutureChainProvider FutureChainProvider => _baseAlgorithm.FutureChainProvider;
279279

280+
/// <summary>
281+
/// Gets the object store, used for persistence
282+
/// </summary>
283+
public IObjectStore ObjectStore => _baseAlgorithm.ObjectStore;
284+
280285
/// <summary>
281286
/// Returns the current Slice object
282287
/// </summary>
@@ -905,6 +910,12 @@ private bool TryConvert<T>(PyObject pyObject, out T result)
905910
/// <param name="api">Initiated API</param>
906911
public void SetApi(IApi api) => _baseAlgorithm.SetApi(api);
907912

913+
/// <summary>
914+
/// Sets the object store
915+
/// </summary>
916+
/// <param name="objectStore">The object store</param>
917+
public void SetObjectStore(IObjectStore objectStore) => _baseAlgorithm.SetObjectStore(objectStore);
918+
908919
/// <summary>
909920
/// Sets the order event provider
910921
/// </summary>

Common/Interfaces/IAlgorithm.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,11 @@ IFutureChainProvider FutureChainProvider
320320
get;
321321
}
322322

323+
/// <summary>
324+
/// Gets the object store, used for persistence
325+
/// </summary>
326+
IObjectStore ObjectStore { get; }
327+
323328
/// <summary>
324329
/// Returns the current Slice object
325330
/// </summary>
@@ -685,6 +690,12 @@ IFutureChainProvider FutureChainProvider
685690
/// <param name="api">Initiated API</param>
686691
void SetApi(IApi api);
687692

693+
/// <summary>
694+
/// Sets the object store
695+
/// </summary>
696+
/// <param name="objectStore">The object store</param>
697+
void SetObjectStore(IObjectStore objectStore);
698+
688699
/// <summary>
689700
/// Sets the order event provider
690701
/// </summary>

Common/Interfaces/IObjectStore.cs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3+
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
using System;
17+
using System.ComponentModel.Composition;
18+
using QuantConnect.Packets;
19+
20+
namespace QuantConnect.Interfaces
21+
{
22+
/// <summary>
23+
/// Provides object storage for data persistence.
24+
/// </summary>
25+
[InheritedExport(typeof(IObjectStore))]
26+
public interface IObjectStore : IDisposable
27+
{
28+
/// <summary>
29+
/// Initializes the object store
30+
/// </summary>
31+
/// <param name="algorithmName">The algorithm name</param>
32+
/// <param name="userId">The user id</param>
33+
/// <param name="projectId">The project id</param>
34+
/// <param name="userToken">The user token</param>
35+
/// <param name="controls">The job controls instance</param>
36+
void Initialize(string algorithmName, int userId, int projectId, string userToken, Controls controls);
37+
38+
/// <summary>
39+
/// Determines whether the store contains data for the specified key
40+
/// </summary>
41+
/// <param name="key">The object key</param>
42+
/// <returns>True if the key was found</returns>
43+
bool ContainsKey(string key);
44+
45+
/// <summary>
46+
/// Returns the object data for the specified key
47+
/// </summary>
48+
/// <param name="key">The object key</param>
49+
/// <returns>A byte array containing the data</returns>
50+
byte[] Read(string key);
51+
52+
/// <summary>
53+
/// Saves the object data for the specified key
54+
/// </summary>
55+
/// <param name="key">The object key</param>
56+
/// <param name="contents">The object data</param>
57+
/// <returns>True if the save operation was successful</returns>
58+
bool Save(string key, byte[] contents);
59+
60+
/// <summary>
61+
/// Deletes the object data for the specified key
62+
/// </summary>
63+
/// <param name="key">The object key</param>
64+
/// <returns>True if the delete operation was successful</returns>
65+
bool Delete(string key);
66+
67+
/// <summary>
68+
/// Returns the file path for the specified key
69+
/// </summary>
70+
/// <param name="key">The object key</param>
71+
/// <returns>The path for the file</returns>
72+
string GetFilePath(string key);
73+
}
74+
}

Common/QuantConnect.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,7 @@
309309
<Compile Include="Indicators\IIndicatorWarmUpPeriodProvider.cs" />
310310
<Compile Include="Interfaces\IAccountCurrencyProvider.cs" />
311311
<Compile Include="Interfaces\IAlgorithmSubscriptionManager.cs" />
312+
<Compile Include="Interfaces\IObjectStore.cs" />
312313
<Compile Include="Interfaces\IBrokerageCashSynchronizer.cs" />
313314
<Compile Include="Interfaces\IBusyCollection.cs" />
314315
<Compile Include="Interfaces\IDataProviderEvents.cs" />
@@ -745,6 +746,8 @@
745746
<Compile Include="Statistics\TradeStatistics.cs" />
746747
<Compile Include="Statistics\Statistics.cs" />
747748
<Compile Include="RealTimeSynchronizedTimer.cs" />
749+
<Compile Include="Storage\LocalObjectStore.cs" />
750+
<Compile Include="Storage\ObjectStore.cs" />
748751
<Compile Include="StringExtensions.cs" />
749752
<Compile Include="Parse.cs" />
750753
<Compile Include="Symbol.cs" />

Common/Storage/LocalObjectStore.cs

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
/*
2+
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3+
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
using System;
17+
using System.IO;
18+
using System.Linq;
19+
using QuantConnect.Configuration;
20+
using QuantConnect.Interfaces;
21+
using QuantConnect.Logging;
22+
using QuantConnect.Packets;
23+
24+
namespace QuantConnect.Storage
25+
{
26+
/// <summary>
27+
/// A local disk implementation of <see cref="IObjectStore"/>.
28+
/// </summary>
29+
public class LocalObjectStore : IObjectStore
30+
{
31+
private static readonly string StorageRoot = Path.GetFullPath(Config.Get("object-store-root", "./storage"));
32+
33+
/// <summary>
34+
/// The root storage folder for the algorithm
35+
/// </summary>
36+
protected string AlgorithmStorageRoot { get; private set; }
37+
38+
/// <summary>
39+
/// Initializes the object store
40+
/// </summary>
41+
/// <param name="algorithmName">The algorithm name</param>
42+
/// <param name="userId">The user id</param>
43+
/// <param name="projectId">The project id</param>
44+
/// <param name="userToken">The user token</param>
45+
/// <param name="controls">The job controls instance</param>
46+
public virtual void Initialize(string algorithmName, int userId, int projectId, string userToken, Controls controls)
47+
{
48+
// absolute path including algorithm name
49+
AlgorithmStorageRoot = Path.Combine(StorageRoot, algorithmName);
50+
51+
// create the root path if it does not exist
52+
Directory.CreateDirectory(AlgorithmStorageRoot);
53+
}
54+
55+
/// <summary>
56+
/// Determines whether the store contains data for the specified key
57+
/// </summary>
58+
/// <param name="key">The object key</param>
59+
/// <returns>True if the key was found</returns>
60+
public bool ContainsKey(string key)
61+
{
62+
if (key == null)
63+
{
64+
throw new ArgumentNullException(nameof(key));
65+
}
66+
67+
try
68+
{
69+
var fileName = GetFilePath(key);
70+
71+
return File.Exists(fileName);
72+
}
73+
catch (Exception exception)
74+
{
75+
Log.Error(exception, $"Error checking file existence, key: [{key}]");
76+
return false;
77+
}
78+
}
79+
80+
/// <summary>
81+
/// Returns the object data for the specified key
82+
/// </summary>
83+
/// <param name="key">The object key</param>
84+
/// <returns>A byte array containing the data</returns>
85+
public byte[] Read(string key)
86+
{
87+
if (key == null)
88+
{
89+
throw new ArgumentNullException(nameof(key));
90+
}
91+
92+
try
93+
{
94+
var fileName = GetFilePath(key);
95+
96+
return File.ReadAllBytes(fileName);
97+
}
98+
catch (Exception exception)
99+
{
100+
Log.Error(exception, $"Error reading file, key: [{key}]");
101+
return null;
102+
}
103+
}
104+
105+
/// <summary>
106+
/// Saves the object data for the specified key
107+
/// </summary>
108+
/// <param name="key">The object key</param>
109+
/// <param name="contents">The object data</param>
110+
/// <returns>True if the save operation was successful</returns>
111+
public bool Save(string key, byte[] contents)
112+
{
113+
if (key == null)
114+
{
115+
throw new ArgumentNullException(nameof(key));
116+
}
117+
118+
try
119+
{
120+
var fileName = GetFilePath(key);
121+
122+
File.WriteAllBytes(fileName, contents);
123+
}
124+
catch (Exception exception)
125+
{
126+
Log.Error(exception, $"Error saving file, key: [{key}]");
127+
return false;
128+
}
129+
130+
return true;
131+
}
132+
133+
/// <summary>
134+
/// Deletes the object data for the specified key
135+
/// </summary>
136+
/// <param name="key">The object key</param>
137+
/// <returns>True if the delete operation was successful</returns>
138+
public bool Delete(string key)
139+
{
140+
if (key == null)
141+
{
142+
throw new ArgumentNullException(nameof(key));
143+
}
144+
145+
try
146+
{
147+
var fileName = GetFilePath(key);
148+
149+
File.Delete(fileName);
150+
}
151+
catch (Exception exception)
152+
{
153+
Log.Error(exception, $"Error deleting file, key: [{key}]");
154+
return false;
155+
}
156+
157+
return true;
158+
}
159+
160+
/// <summary>
161+
/// Returns the file path for the specified key
162+
/// </summary>
163+
/// <param name="key">The object key</param>
164+
/// <returns>The path for the file</returns>
165+
public string GetFilePath(string key)
166+
{
167+
if (key == null)
168+
{
169+
throw new ArgumentNullException(nameof(key));
170+
}
171+
172+
return Path.Combine(AlgorithmStorageRoot, $"{key.ToMD5()}.dat");
173+
}
174+
175+
/// <summary>
176+
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
177+
/// </summary>
178+
public virtual void Dispose()
179+
{
180+
try
181+
{
182+
// if the object store was not used, delete the empty storage directory created in Initialize
183+
if (!Directory.EnumerateFileSystemEntries(AlgorithmStorageRoot).Any())
184+
{
185+
Directory.Delete(AlgorithmStorageRoot);
186+
}
187+
}
188+
catch (Exception exception)
189+
{
190+
Log.Error(exception, "Error deleting storage directory.");
191+
}
192+
}
193+
}
194+
}

0 commit comments

Comments
 (0)