/* * 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.ComponentModel.Design; using System.Globalization; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; using EnvDTE80; using System.Collections.Generic; using System.Linq; using System.IO; using System.Threading; using System.Diagnostics; using System.Threading.Tasks; namespace QuantConnect.VisualStudioPlugin { /// /// Command handler for QuantConnect Solution Explorer menu buttons /// internal sealed class SolutionExplorerMenuCommand { /// /// Command IDs for Solution Explorer menu buttons /// private const int _sendForBacktestingCommandId = 0x0100; private const int _saveToQuantConnectCommandId = 0x0110; /// /// Command menu group (command set GUID). /// private static readonly Guid _commandSet = new Guid("00ce2ccb-74c7-42f4-bf63-52c573fc1532"); /// /// VS Package that provides this command, not null. /// private readonly QuantConnectPackage _package; private readonly DTE2 _dte2; private ProjectFinder _projectFinder; /// /// Observer for the status of backtests launched from the plugin /// private readonly IBacktestObserver _backtestObserver; private readonly AuthenticationCommand _authenticationCommand; /// /// Instance of the solution explorer menu command. /// public static SolutionExplorerMenuCommand Instance { get; private set; } /// /// Gets the service provider from the owner package. /// private IServiceProvider _serviceProvider => _package; // Lazily create _projectFinder only when we have an opened solution private ProjectFinder _lazyProjectFinder => _projectFinder ?? (_projectFinder = CreateProjectFinder()); private ProjectFinder CreateProjectFinder() { return new ProjectFinder(PathUtils.GetSolutionFolder(_dte2)); } /// /// Initializes a new instance of the class. /// Adds our command handlers for menu (commands must exist in the command table file) /// /// Owner package, not null. private SolutionExplorerMenuCommand(Package package, IBacktestObserver backtestObserver) { if (package == null) { throw new ArgumentNullException(nameof(package)); } _backtestObserver = backtestObserver; _package = package as QuantConnectPackage; _dte2 = _serviceProvider.GetService(typeof(SDTE)) as DTE2; _authenticationCommand = new AuthenticationCommand(); var commandService = _serviceProvider.GetService(typeof(IMenuCommandService)) as OleMenuCommandService; if (commandService != null) { RegisterSendForBacktesting(commandService); RegisterSaveToQuantConnect(commandService); } } private void RegisterSendForBacktesting(OleMenuCommandService commandService) { var menuCommandId = new CommandID(_commandSet, _sendForBacktestingCommandId); var oleMenuItem = new OleMenuCommand(SendForBacktestingCallback, menuCommandId); commandService.AddCommand(oleMenuItem); } private void RegisterSaveToQuantConnect(OleMenuCommandService commandService) { var menuCommandId = new CommandID(_commandSet, _saveToQuantConnectCommandId); var oleMenuItem = new OleMenuCommand(SaveToQuantConnectCallback, menuCommandId); commandService.AddCommand(oleMenuItem); } /// /// Initializes the singleton instance of the command. /// /// Owner package, not null. public static void Initialize(Package package, IBacktestObserver backtestObserver) { Instance = new SolutionExplorerMenuCommand(package, backtestObserver); } private void SendForBacktestingCallback(object sender, EventArgs e) { try { ExecuteOnProjectAsync(sender, async (selectedProjectId, selectedProjectName, files) => { var uploadResult = await System.Threading.Tasks.Task.Run(() => UploadFilesToServer(selectedProjectId, files)); if (!uploadResult) { return; } var compilationResult = await CompileProjectOnServer(selectedProjectId); if (!compilationResult.Item1) { var errorDialog = new ErrorDialog("Compilation Error", compilationResult.Item2); VsUtils.DisplayDialogWindow(errorDialog); return; } var backtestResult = await BacktestProjectOnServer(selectedProjectId, compilationResult.Item2); if (!backtestResult.Item1) { var errorDialog = new ErrorDialog("Backtest Failed", backtestResult.Item2.Error); VsUtils.DisplayDialogWindow(errorDialog); return; } var projectUrl = string.Format( CultureInfo.CurrentCulture, "https://www.quantconnect.com/terminal/#open/{0}/{1}", selectedProjectId, backtestResult.Item2.BacktestId ); Process.Start(projectUrl); }); } catch (Exception exception) { VsUtils.ShowErrorMessageBox(_serviceProvider, "QuantConnect Exception", exception.ToString()); } } private void SaveToQuantConnectCallback(object sender, EventArgs e) { try { ExecuteOnProjectAsync(sender, (selectedProjectId, selectedProjectName, files) => { System.Threading.Tasks.Task.Factory.StartNew(() => { UploadFilesToServer(selectedProjectId, files); }, CancellationToken.None, TaskCreationOptions.AttachedToParent, TaskScheduler.Default); }); } catch (Exception exception) { VsUtils.ShowErrorMessageBox(_serviceProvider, "QuantConnect Exception", exception.ToString()); } } /// /// Uploads a list of files to a specific project at QuantConnect /// /// Target project Id /// List of files to upload /// Returns false if any file failed to be uploaded private bool UploadFilesToServer(int selectedProjectId, IEnumerable files) { VsUtils.DisplayInStatusBar(_serviceProvider, "Uploading files to server..."); var api = AuthorizationManager.GetInstance().GetApi(); // Counters to keep track of files uploaded or not var filesUploaded = 0; var filesNotUploaded = 0; foreach (var file in files) { api.DeleteProjectFile(selectedProjectId, file.FileName); try { var fileContent = File.ReadAllText(file.FilePath); var response = api.AddProjectFile(selectedProjectId, file.FileName, fileContent); if (response.Success) { filesUploaded++; } else { VSActivityLog.Error("Failed to add project file " + file.FileName); filesNotUploaded++; } } catch (Exception exception) { VSActivityLog.Error("Exception adding project file " + file.FileName + ". Exception " + exception); filesNotUploaded++; } } // Update Status bar accordingly based on counters var message = "Files upload complete"; message += (filesUploaded != 0) ? ". Uploaded " + filesUploaded : ""; message += (filesNotUploaded != 0) ? ". Failed to upload " + filesNotUploaded : ""; VsUtils.DisplayInStatusBar(_serviceProvider, message); // Return false if any file failed to be uploaded var result = filesNotUploaded == 0; if (!result) { VsUtils.ShowErrorMessageBox(_serviceProvider, "Upload Files Failed", message); } return result; } /// /// Compiles specific projectId at QuantConnect /// /// Target project Id /// Tuple<bool, string>. Item1 is true if compilation succeeded. /// Item2 is compile Id if compilation succeeded else error message. private async Task> CompileProjectOnServer(int projectId) { VsUtils.DisplayInStatusBar(_serviceProvider, "Compiling project..."); var api = AuthorizationManager.GetInstance().GetApi(); var compileStatus = await System.Threading.Tasks.Task.Run(() => api.CreateCompile(projectId)); var compileId = compileStatus.CompileId; while (compileStatus.State == Api.CompileState.InQueue) { compileStatus = await System.Threading.Tasks.Task.Delay(2000). ContinueWith(_ => api.ReadCompile(projectId, compileId)); } if (compileStatus.State == Api.CompileState.BuildError) { // Default to show Errors, now it is coming empty so use Logs. Will only show First Error || Log var error = compileStatus.Errors.Count == 0 ? compileStatus.Logs.FirstOrDefault() : compileStatus.Errors.FirstOrDefault(); VsUtils.DisplayInStatusBar(_serviceProvider, "Error when compiling project"); return new Tuple(false, error); } VsUtils.DisplayInStatusBar(_serviceProvider, "Compilation completed successfully"); return new Tuple(true, compileStatus.CompileId); } /// /// Backtests specific projectId and compileId at QuantConnect /// /// Target project Id /// Target compile Id /// Tuple<bool, string>. Item1 is true if backtest succeeded. /// Item2 is the corresponding Api.Backtest instance private async Task> BacktestProjectOnServer(int projectId, string compileId) { VsUtils.DisplayInStatusBar(_serviceProvider, "Backtesting project..."); var api = AuthorizationManager.GetInstance().GetApi(); var backtestName = BacktestNameProvider.GetNewName(); var backtestStatus = await System.Threading.Tasks.Task.Run(() => api.CreateBacktest(projectId, compileId, backtestName)); var backtestId = backtestStatus.BacktestId; // Notify observer new backtest _backtestObserver.BacktestCreated(projectId, backtestStatus); var errorPresent = false; while (backtestStatus.Progress < 1 && !errorPresent) { backtestStatus = await System.Threading.Tasks.Task.Delay(4000). ContinueWith(_ => api.ReadBacktest(projectId, backtestId)); errorPresent = !string.IsNullOrEmpty(backtestStatus.Error) || !string.IsNullOrEmpty(backtestStatus.StackTrace); // Notify observer backtest status _backtestObserver.BacktestStatusUpdated(projectId, backtestStatus); } // Notify observer backtest finished _backtestObserver.BacktestFinished(projectId, backtestStatus); if (errorPresent) { VsUtils.DisplayInStatusBar(_serviceProvider, "Error when backtesting project"); return new Tuple(false, backtestStatus); } var successMessage = "Backtest completed successfully"; VsUtils.DisplayInStatusBar(_serviceProvider, successMessage); return new Tuple(true, backtestStatus); } private async void ExecuteOnProjectAsync(object sender, Action> onProject) { if (await _authenticationCommand.Login(_serviceProvider, false)) { var projects = await System.Threading.Tasks.Task.Run(() => { var api = AuthorizationManager.GetInstance().GetApi(); return api.ListProjects().Projects; }); var projectNames = projects.Select(p => Tuple.Create(p.ProjectId, p.Name, p.Language)).ToList(); var files = GetSelectedFiles(sender); var fileNames = files.Select(tuple => tuple.FileName).ToList(); var suggestedProjectName = _lazyProjectFinder.ProjectNameForFiles(fileNames); var projectNameDialog = new ProjectNameDialog(projectNames, suggestedProjectName); VsUtils.DisplayDialogWindow(projectNameDialog); if (projectNameDialog.ProjectNameProvided) { var selectedProjectName = projectNameDialog.SelectedProjectName; var selectedProjectId = projectNameDialog.SelectedProjectId; _lazyProjectFinder.AssociateProjectWith(selectedProjectName, fileNames); if (!selectedProjectId.HasValue) { var newProjectLanguage = PathUtils.DetermineProjectLanguage(files.Select(f => f.FilePath).ToList()); if (!newProjectLanguage.HasValue) { VsUtils.ShowMessageBox(_serviceProvider, "Failed to determine project language", $"Failed to determine programming laguage for a project"); return; } selectedProjectId = CreateQuantConnectProject(selectedProjectName, newProjectLanguage.Value); if (!selectedProjectId.HasValue) { VsUtils.ShowMessageBox(_serviceProvider, "Failed to create a project", $"Failed to create a project {selectedProjectName}"); } onProject.Invoke(selectedProjectId.Value, selectedProjectName, files); } else { onProject.Invoke(selectedProjectId.Value, selectedProjectName, files); } } } } private int? CreateQuantConnectProject(string projectName, Language projectLanguage) { var api = AuthorizationManager.GetInstance().GetApi(); var projectResponse = api.CreateProject(projectName, projectLanguage); if (!projectResponse.Success) { return null; } return projectResponse.Projects[0].ProjectId; } private List GetSelectedFiles(object sender) { var selectedFiles = new List(); var selectedItems = (object[])_dte2.ToolWindows.SolutionExplorer.SelectedItems; foreach (EnvDTE.UIHierarchyItem selectedUIHierarchyItem in selectedItems) { if (selectedUIHierarchyItem.Object is EnvDTE.ProjectItem) { var item = selectedUIHierarchyItem.Object as EnvDTE.ProjectItem; var filePath = item.Properties.Item("FullPath").Value.ToString(); var selectedItem = new SelectedItem { FileName = item.Name, FilePath = filePath }; selectedFiles.Add(selectedItem); } } // Check if the user selected a folder, and include files in directories otherwise language inference breaks. // Also to maintain child folder structure on webclient tweak the filename to contain folders expressed in URI format. if (selectedFiles.Count == 1 && string.IsNullOrEmpty(Path.GetExtension(selectedFiles.First().FilePath))) { var basePath = selectedFiles.First().FilePath; var nonFolders = Directory.GetFiles(selectedFiles.First().FilePath, "*", SearchOption.AllDirectories); selectedFiles = nonFolders.Select(c => new SelectedItem { FileName = "/" + c.Replace(basePath, string.Empty).Replace("\\", "/"), FilePath = c }).ToList(); } return selectedFiles; } private class SelectedItem { public string FileName { get; set; } public string FilePath { get; set; } } } }