qc 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: ef59d64a4ce89a1850fbd7b90923c78340d925b7
4
+ data.tar.gz: 6e0aa75773f5e068567e2637f3ce2791a229d1ae
5
+ SHA512:
6
+ metadata.gz: 856d682bb7fc6fcc23b060a157ec53168a263bd16f536babf35639fa8afa8f03c446d127505d05cc1f3bc1318cdeb5da54f317e691d7459d0dda0de5642730dd
7
+ data.tar.gz: 6be35bcf5140b01050323135e193b2c8a766c3af18baac921c61eeb55d4b23d7eb1a50921cea829af063d75a3f2b38154e690cfb7a90ff603cba3616312d5991
@@ -0,0 +1,50 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /spec/examples.txt
9
+ /test/tmp/
10
+ /test/version_tmp/
11
+ /tmp/
12
+
13
+ # Used by dotenv library to load environment variables.
14
+ # .env
15
+
16
+ ## Specific to RubyMotion:
17
+ .dat*
18
+ .repl_history
19
+ build/
20
+ *.bridgesupport
21
+ build-iPhoneOS/
22
+ build-iPhoneSimulator/
23
+
24
+ ## Specific to RubyMotion (use of CocoaPods):
25
+ #
26
+ # We recommend against adding the Pods directory to your .gitignore. However
27
+ # you should judge for yourself, the pros and cons are mentioned at:
28
+ # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
29
+ #
30
+ # vendor/Pods/
31
+
32
+ ## Documentation cache and generated files:
33
+ /.yardoc/
34
+ /_yardoc/
35
+ /doc/
36
+ /rdoc/
37
+
38
+ ## Environment normalization:
39
+ /.bundle/
40
+ /vendor/bundle
41
+ /lib/bundler/man/
42
+
43
+ # for a library or gem, you might want to ignore these files since the code is
44
+ # intended to run in multiple environments; otherwise, check them in:
45
+ # Gemfile.lock
46
+ # .ruby-version
47
+ # .ruby-gemset
48
+
49
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
50
+ .rvmrc
@@ -0,0 +1 @@
1
+ 2.4.2
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in qc.gemspec
6
+ gemspec
@@ -0,0 +1,54 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ qc (0.5.1)
5
+ rest-client (~> 2.0.2)
6
+ vcr (~> 3.0.3)
7
+ webmock (~> 3.1.0)
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ addressable (2.5.2)
13
+ public_suffix (>= 2.0.2, < 4.0)
14
+ awesome_print (1.8.0)
15
+ crack (0.4.3)
16
+ safe_yaml (~> 1.0.0)
17
+ domain_name (0.5.20170404)
18
+ unf (>= 0.0.5, < 1.0.0)
19
+ hashdiff (0.3.7)
20
+ http-cookie (1.0.3)
21
+ domain_name (~> 0.5)
22
+ mime-types (3.1)
23
+ mime-types-data (~> 3.2015)
24
+ mime-types-data (3.2016.0521)
25
+ minitest (5.10.3)
26
+ netrc (0.11.0)
27
+ public_suffix (3.0.0)
28
+ rake (10.5.0)
29
+ rest-client (2.0.2)
30
+ http-cookie (>= 1.0.2, < 2.0)
31
+ mime-types (>= 1.16, < 4.0)
32
+ netrc (~> 0.8)
33
+ safe_yaml (1.0.4)
34
+ unf (0.1.4)
35
+ unf_ext
36
+ unf_ext (0.0.7.4)
37
+ vcr (3.0.3)
38
+ webmock (3.1.0)
39
+ addressable (>= 2.3.6)
40
+ crack (>= 0.3.2)
41
+ hashdiff
42
+
43
+ PLATFORMS
44
+ ruby
45
+
46
+ DEPENDENCIES
47
+ awesome_print
48
+ bundler (~> 1.15)
49
+ minitest (~> 5.0)
50
+ qc!
51
+ rake (~> 10.0)
52
+
53
+ BUNDLED WITH
54
+ 1.15.4
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Jorge Manrubia
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,56 @@
1
+ # Qc
2
+
3
+ Qc is a commandline tool that lets you sync and run your [QuantConnect](https://www.quantconnect.com) backtests from the command line.
4
+
5
+ ## Installation
6
+
7
+ Qc is distributed as a ruby gem. You can install it with:
8
+
9
+ ```
10
+ gem install qc
11
+ ```
12
+
13
+ To update it run:
14
+
15
+ ```
16
+ gem update qc
17
+ ```
18
+
19
+ ## Workflow
20
+
21
+ 1. Run `qc login` for introducing your QuantConnect credentials
22
+ 2. Run `qc init` on the directory that contains your QuantConnect algorithm
23
+ 3. Execute `qc` to sync and backtest your algorithm in QuantConnect
24
+
25
+ ## Usage
26
+
27
+ ```
28
+ qc [command]
29
+ ```
30
+
31
+ The supported commands are:
32
+
33
+ | Command| description|
34
+ | -- | -- |
35
+ | `qc login`| It will ask for the api credentials you can find in [your QuantConnect account page](https://www.quantconnect.com/account). They will be stored in `~/.qc`. You only need to login once. |
36
+ | `qc logout`| Logout from QuantConnect clearing the credentials stored locally. |
37
+ | `qc init`| Initialize the directory of the algo project you are working on. It will ask for a QuantConnect project to link your algo with. You need to run this once for every project you want to sync with QuantConnect. |
38
+ | `qc push` | Send your local files to QuantConnect. It will only send the files that changed since the last time you run the command |
39
+ | `qc compile` | Compile your project in QuantConnect |
40
+ | `qc backtest` | Run the backtest of your algorithm in QuantConnect |
41
+
42
+ When no command is provided, it will execute `push`, `compile` and `backtest` in sequence.
43
+
44
+ ## Development
45
+
46
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
47
+
48
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
49
+
50
+ ## Contributing
51
+
52
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/qc.
53
+
54
+ ## License
55
+
56
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "qc"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/qc ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
4
+ require 'qc'
5
+
6
+ status = Qc::Console.new.run(ARGV) ? 0 : 1
7
+ exit status
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,12 @@
1
+ require 'fileutils'
2
+ require 'yaml'
3
+ require 'rest-client'
4
+ require 'json'
5
+ require 'awesome_print'
6
+ require 'optparse'
7
+
8
+ Dir[File.join(__dir__, "qc/**/*.rb")].each { |f| require f }
9
+
10
+ module Qc
11
+ # Your code goes here...
12
+ end
@@ -0,0 +1,14 @@
1
+ module Qc
2
+ class Backtest < Struct.new(:id, :name, :completed, :progress, :result, :success)
3
+ alias success? success
4
+ alias completed? completed
5
+
6
+ def error?
7
+ !success?
8
+ end
9
+
10
+ def progress_in_percentage
11
+ progress * 100
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,232 @@
1
+ module Qc
2
+ class CommandRunner
3
+ DEFAULT_FILE_EXTENSIONS = 'cs,py'
4
+
5
+ SUPPORTED_COMMANDS =%i(login logout init push compile backtest)
6
+ COMPILE_POLLING_DELAY_IN_SECONDS = 2
7
+ BACKTEST_DELAY_IN_SECONDS = 2
8
+
9
+ attr_reader :quant_connect_proxy
10
+ attr_accessor :project_settings
11
+
12
+ def initialize(quant_connect_proxy)
13
+ @quant_connect_proxy = quant_connect_proxy
14
+ @project_settings = read_project_settings
15
+ end
16
+
17
+ def run(command)
18
+ if command == :login
19
+ do_run(command)
20
+ else
21
+ require_login do
22
+ do_run(command)
23
+ end
24
+ end
25
+ end
26
+
27
+ def run_default
28
+ run(:default)
29
+ end
30
+
31
+ private
32
+
33
+ def credentials
34
+ quant_connect_proxy&.credentials
35
+ end
36
+
37
+ def credentials=(new_credentials)
38
+ quant_connect_proxy.credentials = new_credentials
39
+ end
40
+
41
+ def read_project_settings
42
+ if ::File.exist?(project_settings_file)
43
+ YAML.load(::File.open(project_settings_file))
44
+ else
45
+ Qc::ProjectSettings.new
46
+ end
47
+ end
48
+
49
+ def logged_in?
50
+ !!credentials
51
+ end
52
+
53
+ def require_login
54
+ if credentials
55
+ yield
56
+ else
57
+ puts "Please sign in by executing 'qc login' first"
58
+ false
59
+ end
60
+ end
61
+
62
+ def do_run(command)
63
+ case command
64
+ when :default
65
+ do_run_default
66
+ when *SUPPORTED_COMMANDS
67
+ send "run_#{command}"
68
+ else
69
+ raise "Unknonw command '#{command}'. Supported commands: #{SUPPORTED_COMMANDS.collect(&:to_s).join(', ')}"
70
+ end
71
+ end
72
+
73
+ def run_login
74
+ puts "Please introduce your QuantConnect API credentials. You can find them in your preferences in https://www.quantconnect.com/account."
75
+ user_id = ask_for_value 'User id:'
76
+ access_token = ask_for_value 'Access token:'
77
+
78
+ self.credentials = Qc::Credentials.new(user_id, access_token)
79
+
80
+ if valid_login?
81
+ credentials.save_to_home
82
+ true
83
+ else
84
+ puts "Invalid credentials"
85
+ false
86
+ end
87
+ end
88
+
89
+ def run_init
90
+ FileUtils.mkdir_p(Qc::Util.project_dir)
91
+ project = ask_for_project
92
+ self.project_settings.project_id = project.id
93
+ self.project_settings.file_extensions = ask_for_extensions
94
+ save_project_settings
95
+ true
96
+ end
97
+
98
+ def run_logout
99
+ credentials.destroy
100
+ puts "Logged out successfully"
101
+ true
102
+ end
103
+
104
+ def run_push
105
+ sync_changed_files
106
+ save_current_timestamp
107
+ true
108
+ end
109
+
110
+ def run_compile
111
+ compile = quant_connect_proxy.create_compile project_settings.project_id
112
+ puts "Compile request sent to the queue with id #{compile.id}"
113
+
114
+ begin
115
+ puts "Waiting for compilation result..."
116
+ compile = quant_connect_proxy.read_compile project_settings.project_id, compile.id
117
+ sleep COMPILE_POLLING_DELAY_IN_SECONDS if compile.in_queue?
118
+ end while compile.in_queue?
119
+
120
+ puts "Compile success" if compile.success?
121
+ puts "Compile failed" if compile.error?
122
+
123
+ project_settings.last_compile_id = compile.id
124
+ save_project_settings
125
+
126
+ compile.success?
127
+ end
128
+
129
+ def run_backtest
130
+ unless project_settings.last_compile_id
131
+ puts "Project not compiled. Please run 'qc compile'"
132
+ return false
133
+ end
134
+
135
+ do_run_backtest
136
+
137
+ true
138
+ end
139
+
140
+ def do_run_default
141
+ failed = %i(push compile backtest).find do |command|
142
+ !run(command)
143
+ end
144
+
145
+ !failed
146
+ end
147
+
148
+ def do_run_backtest
149
+ backtest = quant_connect_proxy.create_backtest project_settings.project_id, project_settings.last_compile_id, "backtest-#{project_settings.last_compile_id}"
150
+ puts "Backtest for compile #{project_settings.last_compile_id} sent to the queue with id #{backtest.id}"
151
+
152
+ begin
153
+ puts "Waiting for backtest to finish (#{backtest.progress_in_percentage}\% completed)..."
154
+ backtest = quant_connect_proxy.read_backtest project_settings.project_id, backtest.id
155
+ sleep BACKTEST_DELAY_IN_SECONDS if backtest.completed?
156
+ end while !backtest.completed?
157
+
158
+ puts "RESULT: #{backtest.result}"
159
+
160
+ puts "Bactest finished" if backtest.success?
161
+ puts "Backtest failed" if backtest.error?
162
+
163
+ project_settings.last_backtest_id = backtest.id
164
+ save_project_settings
165
+ end
166
+
167
+ def valid_login?
168
+ quant_connect_proxy.valid_login?
169
+ end
170
+
171
+ def save_project_settings
172
+ ::File.open(project_settings_file, 'w') {|file| file.write self.project_settings.to_yaml}
173
+ end
174
+
175
+ def project_settings_file
176
+ ::File.join(Qc::Util.project_dir, 'settings.yml')
177
+ end
178
+
179
+ def ask_for_value(question)
180
+ puts question
181
+ v = STDIN.gets
182
+ v.chomp
183
+ end
184
+
185
+ def ask_for_project
186
+ puts "Fetching projets from Quantconnect..."
187
+ projects = quant_connect_proxy.list_projects
188
+ puts "Select the project you want to associate with this directory"
189
+ projects.each.with_index do |project, index|
190
+ puts "[#{index+1}] - #{project.name}"
191
+ end
192
+ index = ask_for_value "Project number?"
193
+ index = index.to_i
194
+ if index >=1 && index < projects.length + 1
195
+ projects[index-1]
196
+ else
197
+ puts "Invalid value (please type a number between #{1} and #{projects.length})"
198
+ ask_for_project
199
+ end
200
+ end
201
+
202
+ def ask_for_extensions
203
+ file_extensions = ask_for_value "Introduce the file extensions you want to send to QuantConnect as a comma separated list. ENTER to default '#{DEFAULT_FILE_EXTENSIONS}'"
204
+ file_extensions = DEFAULT_FILE_EXTENSIONS if file_extensions.empty?
205
+ file_extensions
206
+ end
207
+
208
+
209
+ def sync_changed_files
210
+ changed_files.each do |file|
211
+ puts "uploading #{file}..."
212
+ content = ::File.read file
213
+ quant_connect_proxy.put_file project_settings.project_id, file, content
214
+ end
215
+ end
216
+
217
+ def changed_files
218
+ all_files = Dir["*.{#{project_settings.file_extensions}}"]
219
+
220
+ return all_files unless project_settings.last_sync_at
221
+
222
+ all_files.find_all do |file|
223
+ ::File.mtime(file) > project_settings.last_sync_at
224
+ end
225
+ end
226
+
227
+ def save_current_timestamp
228
+ project_settings.last_sync_at = Time.now
229
+ save_project_settings
230
+ end
231
+ end
232
+ end
@@ -0,0 +1,15 @@
1
+ module Qc
2
+ class Compile < Struct.new(:id, :state)
3
+ def in_queue?
4
+ state == 'InQueue'
5
+ end
6
+
7
+ def success?
8
+ state == 'BuildSuccess'
9
+ end
10
+
11
+ def error?
12
+ state == 'BuildError'
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,59 @@
1
+ module Qc
2
+ class Console
3
+ def initialize
4
+ end
5
+
6
+ def run(argv)
7
+ options = parse_options(argv)
8
+
9
+ command = argv[0]
10
+ client = Qc::CommandRunner.new(quant_connect_proxy)
11
+ success = begin
12
+ if command
13
+ client.run(command.to_sym)
14
+ else
15
+ client.run_default
16
+ end
17
+ rescue StandardError => error
18
+ puts "Error: #{error}"
19
+ false
20
+ end
21
+
22
+ success
23
+ end
24
+
25
+ private
26
+
27
+ def quant_connect_proxy
28
+ @quant_connect_proxy ||= Qc::QuantConnectProxy.new(Qc::Credentials.read_from_home)
29
+ end
30
+
31
+ def parse_options(args)
32
+ options = OpenStruct.new
33
+
34
+ opt_parser = OptionParser.new do |opts|
35
+ opts.banner = "Usage: qc [command] [options]"
36
+
37
+ opts.separator ""
38
+ opts.separator "Supported commands: #{Qc::CommandRunner::SUPPORTED_COMMANDS.join(', ')}"
39
+ opts.separator "When no command provided it will execute: 'push compile backtest' in sequence"
40
+
41
+ opts.separator ""
42
+ opts.separator "Common options:"
43
+
44
+ opts.on("-h", "--help", "Show this message") do
45
+ puts opts
46
+ exit 0
47
+ end
48
+
49
+ opts.on("--version", "Show version") do
50
+ puts Qc::VERSION
51
+ exit 0
52
+ end
53
+ end
54
+
55
+ opt_parser.parse!(args)
56
+ options
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,25 @@
1
+ module Qc
2
+ class Credentials < Struct.new(:user_id, :access_token)
3
+ FILE_NAME = 'credentials.yml'
4
+
5
+ def self.read_from_home
6
+ return nil unless ::File.exists?(credentials_file)
7
+ YAML.load_file credentials_file
8
+ end
9
+
10
+ def save_to_home
11
+ FileUtils.mkdir_p(Qc::Util.home_dir)
12
+ ::File.open(self.class.credentials_file, 'w') do |file|
13
+ file.write self.to_yaml
14
+ end
15
+ end
16
+
17
+ def destroy
18
+ FileUtils.remove(self.class.credentials_file)
19
+ end
20
+
21
+ def self.credentials_file
22
+ ::File.join(Qc::Util.home_dir, FILE_NAME)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,5 @@
1
+ module Qc
2
+ class File < Struct.new(:name, :content)
3
+
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module Qc
2
+ class Project < Struct.new(:id, :name)
3
+
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module Qc
2
+ class ProjectSettings< Struct.new(:project_id, :file_extensions, :last_sync_at, :last_compile_id, :last_backtest_id)
3
+
4
+ end
5
+ end
@@ -0,0 +1,120 @@
1
+ module Qc
2
+ class QuantConnectProxy
3
+ class RequestError < StandardError;
4
+ end
5
+
6
+ attr_accessor :credentials
7
+
8
+ BASE_URL = "https://www.quantconnect.com/api/v2"
9
+
10
+ def initialize(credentials)
11
+ @credentials = credentials
12
+ end
13
+
14
+ def valid_login?
15
+ response = perform_request :get, '/authenticate'
16
+ response.success
17
+ end
18
+
19
+ def list_projects
20
+ response = perform_request :get, '/projects/read'
21
+ validate_response! response
22
+ response.projects.collect do |project_json|
23
+ Qc::Project.new(project_json['projectId'], project_json['name'])
24
+ end
25
+ end
26
+
27
+ def delete_project(project_id)
28
+ response = perform_request :post, '/projects/delete', payload: {projectId: project_id}
29
+ validate_response! response
30
+ project_id
31
+ end
32
+
33
+ def create_project(project_name, language: 'C#')
34
+ response = perform_request :post, '/projects/create', payload: {name: project_name, language: language}
35
+ validate_response! response
36
+ project = response.projects[0]
37
+ Qc::Project.new(project['project_id'], project['name'])
38
+ end
39
+
40
+ def put_file(project_id, file_name, file_content)
41
+ payload = {projectId: project_id, name: file_name, content: file_content}
42
+ response = perform_request :post, '/files/update', payload: payload
43
+ if missing_file_error?(response)
44
+ response = perform_request :post, '/files/create', payload: payload
45
+ end
46
+ validate_response! response
47
+ create_file_from_json_response(response.files[0])
48
+ end
49
+
50
+ def read_file(project_id, file_name)
51
+ response = perform_request :post, '/files/read', payload: {projectId: project_id, name: file_name}
52
+ validate_response! response
53
+ create_file_from_json_response(response.files[0])
54
+ end
55
+
56
+ def create_compile(project_id)
57
+ response = perform_request :post, '/compile/create', payload: {projectId: project_id}
58
+ validate_response! response
59
+ create_compile_from_json_response(response)
60
+ end
61
+
62
+ def read_compile(project_id, compile_id)
63
+ response = perform_request :get, '/compile/read', params: {projectId: project_id, compileId: compile_id}
64
+ validate_response! response
65
+ create_compile_from_json_response(response)
66
+ end
67
+
68
+ def create_backtest(project_id, compile_id, backtest_name)
69
+ response = perform_request :post, '/backtests/create', payload: {projectId: project_id, compileId: compile_id, backtestName: backtest_name}
70
+ validate_response! response
71
+ create_backtest_from_json_response(response)
72
+ end
73
+
74
+ def read_backtest(project_id, backtest_id)
75
+ response = perform_request :get, '/backtests/read', params: {projectId: project_id, compileId: backtest_id}
76
+ validate_response! response
77
+ create_backtest_from_json_response(OpenStruct.new({success: response.success}.merge(response.backtests[0])))
78
+ end
79
+
80
+ private
81
+
82
+ def create_backtest_from_json_response(response)
83
+ Qc::Backtest.new(response.backtestId, response.name, response.completed, response.progres.to_f, response.result, response.success)
84
+ end
85
+
86
+ def create_compile_from_json_response(response)
87
+ Qc::Compile.new(response.compileId, response.state)
88
+ end
89
+
90
+ def missing_file_error?(response)
91
+ !response.success && (response.errors.join("\n") =~ /file not found/i)
92
+ end
93
+
94
+ def create_file_from_json_response(file_json)
95
+ Qc::File.new(file_json['name'], file_json['content'])
96
+ end
97
+
98
+ def perform_request(method, path, payload: {}, params: {})
99
+ timestamp = Time.now.utc.to_time.to_i
100
+ password_hash = Digest::SHA256.hexdigest "#{credentials.access_token}:#{timestamp}"
101
+ headers = {Timestamp: timestamp}.merge(params: params)
102
+ response = RestClient::Request.execute method: method,
103
+ url: "#{BASE_URL}#{path}",
104
+ headers: headers,
105
+ user: credentials.user_id,
106
+ content_type: :json,
107
+ accept: :json,
108
+ password: password_hash,
109
+ payload: payload
110
+ body = response.body.empty? ? '{"success": false}' : response.body
111
+ OpenStruct.new JSON.parse(body)
112
+ end
113
+
114
+ def validate_response!(response)
115
+ unless response.success
116
+ raise RequestError, response.inspect
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,12 @@
1
+ module Qc
2
+ class Util
3
+ def self.home_dir
4
+ # Not using `Dir.home` because aruba won't let you mock it
5
+ ::File.join(ENV['HOME'], '.qc')
6
+ end
7
+
8
+ def self.project_dir
9
+ ::File.join('.', '.qc')
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,3 @@
1
+ module Qc
2
+ VERSION = "0.5.1"
3
+ end
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "qc/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "qc"
8
+ spec.version = Qc::VERSION
9
+ spec.authors = ["Jorge Manrubia"]
10
+ spec.email = ["jorge.manrubia@gmail.com"]
11
+
12
+ spec.summary = %q{Sync and run your QuantConnect backtests from the command line}
13
+ spec.homepage = "https://github.com/jorgemanrubia/qc"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
17
+ f.match(%r{^(test|spec|features)/})
18
+ end
19
+ spec.bindir = "exe"
20
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
+ spec.require_paths = ["lib"]
22
+
23
+ spec.add_runtime_dependency "rest-client", "~> 2.0.2"
24
+ spec.add_runtime_dependency "vcr", "~> 3.0.3"
25
+ spec.add_runtime_dependency "webmock", "~> 3.1.0"
26
+ spec.add_development_dependency "awesome_print"
27
+ spec.add_development_dependency "bundler", "~> 1.15"
28
+ spec.add_development_dependency "rake", "~> 10.0"
29
+ spec.add_development_dependency "minitest", "~> 5.0"
30
+ end
metadata ADDED
@@ -0,0 +1,165 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: qc
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.1
5
+ platform: ruby
6
+ authors:
7
+ - Jorge Manrubia
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-11-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rest-client
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 2.0.2
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 2.0.2
27
+ - !ruby/object:Gem::Dependency
28
+ name: vcr
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 3.0.3
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 3.0.3
41
+ - !ruby/object:Gem::Dependency
42
+ name: webmock
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 3.1.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 3.1.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: awesome_print
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: bundler
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.15'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.15'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '10.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '10.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: minitest
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '5.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '5.0'
111
+ description:
112
+ email:
113
+ - jorge.manrubia@gmail.com
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - ".gitignore"
119
+ - ".ruby-version"
120
+ - Gemfile
121
+ - Gemfile.lock
122
+ - LICENSE.txt
123
+ - README.md
124
+ - Rakefile
125
+ - bin/console
126
+ - bin/qc
127
+ - bin/setup
128
+ - lib/qc.rb
129
+ - lib/qc/backtest.rb
130
+ - lib/qc/command_runner.rb
131
+ - lib/qc/compile.rb
132
+ - lib/qc/console.rb
133
+ - lib/qc/credentials.rb
134
+ - lib/qc/file.rb
135
+ - lib/qc/project.rb
136
+ - lib/qc/project_settings.rb
137
+ - lib/qc/quant_connect_proxy.rb
138
+ - lib/qc/util.rb
139
+ - lib/qc/version.rb
140
+ - qc.gemspec
141
+ homepage: https://github.com/jorgemanrubia/qc
142
+ licenses:
143
+ - MIT
144
+ metadata: {}
145
+ post_install_message:
146
+ rdoc_options: []
147
+ require_paths:
148
+ - lib
149
+ required_ruby_version: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - ">="
152
+ - !ruby/object:Gem::Version
153
+ version: '0'
154
+ required_rubygems_version: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - ">="
157
+ - !ruby/object:Gem::Version
158
+ version: '0'
159
+ requirements: []
160
+ rubyforge_project:
161
+ rubygems_version: 2.6.13
162
+ signing_key:
163
+ specification_version: 4
164
+ summary: Sync and run your QuantConnect backtests from the command line
165
+ test_files: []