sqa 0.0.32 → 0.0.37
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +92 -1
- data/README.md +4 -0
- data/Rakefile +52 -10
- data/docs/IMPROVEMENT_PLAN.md +531 -0
- data/docs/advanced/index.md +1 -13
- data/docs/api/index.md +547 -61
- data/docs/api-reference/alphavantageapi.md +1057 -0
- data/docs/api-reference/apierror.md +31 -0
- data/docs/api-reference/index.md +221 -0
- data/docs/api-reference/notimplemented.md +27 -0
- data/docs/api-reference/sqa.md +267 -0
- data/docs/api-reference/sqa_backtest.md +137 -0
- data/docs/api-reference/sqa_backtest_results.md +530 -0
- data/docs/api-reference/sqa_badparametererror.md +13 -0
- data/docs/api-reference/sqa_config.md +538 -0
- data/docs/api-reference/sqa_configurationerror.md +13 -0
- data/docs/api-reference/sqa_datafetcherror.md +56 -0
- data/docs/api-reference/sqa_dataframe.md +752 -0
- data/docs/api-reference/sqa_dataframe_alphavantage.md +30 -0
- data/docs/api-reference/sqa_dataframe_data.md +325 -0
- data/docs/api-reference/sqa_dataframe_yahoofinance.md +25 -0
- data/docs/api-reference/sqa_ensemble.md +413 -0
- data/docs/api-reference/sqa_fpop.md +211 -0
- data/docs/api-reference/sqa_geneticprogram.md +325 -0
- data/docs/api-reference/sqa_geneticprogram_individual.md +114 -0
- data/docs/api-reference/sqa_marketregime.md +212 -0
- data/docs/api-reference/sqa_multitimeframe.md +227 -0
- data/docs/api-reference/sqa_patternmatcher.md +195 -0
- data/docs/api-reference/sqa_pluginmanager.md +55 -0
- data/docs/api-reference/sqa_portfolio.md +455 -0
- data/docs/api-reference/sqa_portfolio_position.md +220 -0
- data/docs/api-reference/sqa_portfolio_trade.md +332 -0
- data/docs/api-reference/sqa_portfoliooptimizer.md +248 -0
- data/docs/api-reference/sqa_riskmanager.md +388 -0
- data/docs/api-reference/sqa_seasonalanalyzer.md +121 -0
- data/docs/api-reference/sqa_sectoranalyzer.md +163 -0
- data/docs/api-reference/sqa_stock.md +649 -0
- data/docs/api-reference/sqa_strategy.md +178 -0
- data/docs/api-reference/sqa_strategy_bollingerbands.md +26 -0
- data/docs/api-reference/sqa_strategy_common.md +29 -0
- data/docs/api-reference/sqa_strategy_consensus.md +129 -0
- data/docs/api-reference/sqa_strategy_ema.md +41 -0
- data/docs/api-reference/sqa_strategy_kbs.md +154 -0
- data/docs/api-reference/sqa_strategy_macd.md +26 -0
- data/docs/api-reference/sqa_strategy_mp.md +41 -0
- data/docs/api-reference/sqa_strategy_mr.md +41 -0
- data/docs/api-reference/sqa_strategy_random.md +41 -0
- data/docs/api-reference/sqa_strategy_rsi.md +41 -0
- data/docs/api-reference/sqa_strategy_sma.md +41 -0
- data/docs/api-reference/sqa_strategy_stochastic.md +26 -0
- data/docs/api-reference/sqa_strategy_volumebreakout.md +26 -0
- data/docs/api-reference/sqa_strategygenerator.md +298 -0
- data/docs/api-reference/sqa_strategygenerator_pattern.md +264 -0
- data/docs/api-reference/sqa_strategygenerator_patterncontext.md +326 -0
- data/docs/api-reference/sqa_strategygenerator_profitablepoint.md +424 -0
- data/docs/api-reference/sqa_stream.md +256 -0
- data/docs/api-reference/sqa_ticker.md +175 -0
- data/docs/api-reference/string.md +135 -0
- data/docs/assets/images/advanced-workflow.svg +89 -0
- data/docs/assets/images/architecture.svg +107 -0
- data/docs/assets/images/data-flow.svg +138 -0
- data/docs/assets/images/getting-started-workflow.svg +88 -0
- data/docs/assets/images/strategy-flow.svg +78 -0
- data/docs/assets/images/system-architecture.svg +150 -0
- data/docs/concepts/index.md +292 -19
- data/docs/getting-started/index.md +1 -14
- data/docs/index.md +26 -23
- data/docs/llms.txt +109 -0
- data/docs/strategies/kbs.md +15 -14
- data/docs/strategy.md +381 -3
- data/docs/terms_of_use.md +1 -1
- data/examples/README.md +10 -0
- data/lib/api/alpha_vantage_api.rb +3 -7
- data/lib/sqa/config.rb +109 -28
- data/lib/sqa/data_frame/data.rb +13 -1
- data/lib/sqa/data_frame.rb +168 -26
- data/lib/sqa/errors.rb +79 -17
- data/lib/sqa/init.rb +70 -15
- data/lib/sqa/pattern_matcher.rb +4 -4
- data/lib/sqa/portfolio.rb +1 -1
- data/lib/sqa/sector_analyzer.rb +3 -11
- data/lib/sqa/stock.rb +169 -15
- data/lib/sqa/strategy.rb +62 -4
- data/lib/sqa/ticker.rb +106 -48
- data/lib/sqa/version.rb +1 -1
- data/lib/sqa.rb +4 -4
- data/mkdocs.yml +68 -81
- metadata +89 -21
- data/docs/README.md +0 -43
- data/examples/sinatra_app/Gemfile +0 -42
- data/examples/sinatra_app/Gemfile.lock +0 -268
- data/examples/sinatra_app/QUICKSTART.md +0 -169
- data/examples/sinatra_app/README.md +0 -471
- data/examples/sinatra_app/RUNNING_WITHOUT_TALIB.md +0 -90
- data/examples/sinatra_app/TROUBLESHOOTING.md +0 -95
- data/examples/sinatra_app/app.rb +0 -404
- data/examples/sinatra_app/config.ru +0 -5
- data/examples/sinatra_app/public/css/style.css +0 -723
- data/examples/sinatra_app/public/debug_macd.html +0 -82
- data/examples/sinatra_app/public/js/app.js +0 -107
- data/examples/sinatra_app/start.sh +0 -53
- data/examples/sinatra_app/views/analyze.erb +0 -306
- data/examples/sinatra_app/views/backtest.erb +0 -325
- data/examples/sinatra_app/views/dashboard.erb +0 -831
- data/examples/sinatra_app/views/error.erb +0 -58
- data/examples/sinatra_app/views/index.erb +0 -118
- data/examples/sinatra_app/views/layout.erb +0 -61
- data/examples/sinatra_app/views/portfolio.erb +0 -43
data/lib/sqa/init.rb
CHANGED
|
@@ -1,12 +1,35 @@
|
|
|
1
1
|
# sqa/lib/sqa/init.rb
|
|
2
2
|
|
|
3
|
+
# SQA (Simple Qualitative Analysis) - Ruby library for stock market technical analysis.
|
|
4
|
+
# Provides high-performance data structures, trading strategies, and integrates with
|
|
5
|
+
# the sqa-tai gem for 150+ technical indicators.
|
|
6
|
+
#
|
|
7
|
+
# @example Basic usage
|
|
8
|
+
# require 'sqa'
|
|
9
|
+
# SQA.init
|
|
10
|
+
# stock = SQA::Stock.new(ticker: 'AAPL')
|
|
11
|
+
# prices = stock.df["adj_close_price"].to_a
|
|
12
|
+
#
|
|
13
|
+
# @example With configuration
|
|
14
|
+
# SQA.init
|
|
15
|
+
# SQA.config.data_dir = "~/my_data"
|
|
16
|
+
# SQA.config.lazy_update = true
|
|
17
|
+
#
|
|
3
18
|
module SQA
|
|
4
19
|
class << self
|
|
5
|
-
|
|
6
|
-
|
|
20
|
+
# @!attribute [w] config
|
|
21
|
+
# @return [SQA::Config] Configuration instance
|
|
22
|
+
attr_writer :config
|
|
7
23
|
|
|
8
|
-
# Initializes the SQA
|
|
9
|
-
#
|
|
24
|
+
# Initializes the SQA library.
|
|
25
|
+
# Should be called once at application startup.
|
|
26
|
+
#
|
|
27
|
+
# @param argv [Array<String>, String] Command line arguments (default: ARGV)
|
|
28
|
+
# @return [SQA::Config] The configuration instance
|
|
29
|
+
#
|
|
30
|
+
# @example
|
|
31
|
+
# SQA.init
|
|
32
|
+
# SQA.init("--debug --verbose")
|
|
10
33
|
#
|
|
11
34
|
def init(argv=ARGV)
|
|
12
35
|
if argv.is_a? String
|
|
@@ -15,10 +38,10 @@ module SQA
|
|
|
15
38
|
|
|
16
39
|
|
|
17
40
|
# Ran at SQA::Config elaboration time
|
|
18
|
-
#
|
|
41
|
+
# @config = Config.new
|
|
19
42
|
|
|
20
43
|
if defined? CLI
|
|
21
|
-
CLI.run! #
|
|
44
|
+
CLI.run! # CLI handles its own argument parsing
|
|
22
45
|
else
|
|
23
46
|
# There are no real command line parameters
|
|
24
47
|
# because the sqa gem is being required within
|
|
@@ -30,29 +53,61 @@ module SQA
|
|
|
30
53
|
config
|
|
31
54
|
end
|
|
32
55
|
|
|
56
|
+
# Returns the Alpha Vantage API key.
|
|
57
|
+
# Reads from AV_API_KEY or ALPHAVANTAGE_API_KEY environment variables.
|
|
58
|
+
#
|
|
59
|
+
# @return [String] The API key
|
|
60
|
+
# @raise [SQA::ConfigurationError] If no API key is set
|
|
33
61
|
def av_api_key
|
|
34
|
-
|
|
62
|
+
@av_api_key ||= ENV['AV_API_KEY'] || ENV['ALPHAVANTAGE_API_KEY']
|
|
63
|
+
@av_api_key || raise(SQA::ConfigurationError, 'Alpha Vantage API key not set. Set AV_API_KEY or ALPHAVANTAGE_API_KEY environment variable.')
|
|
35
64
|
end
|
|
36
65
|
|
|
37
|
-
#
|
|
66
|
+
# Sets the Alpha Vantage API key.
|
|
67
|
+
#
|
|
68
|
+
# @param key [String] The API key to set
|
|
69
|
+
# @return [String] The key that was set
|
|
70
|
+
def av_api_key=(key)
|
|
71
|
+
@av_api_key = key
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Legacy accessor for backward compatibility with SQA.av.key usage.
|
|
75
|
+
#
|
|
76
|
+
# @return [SQA] Self, to allow SQA.av.key calls
|
|
38
77
|
def av
|
|
39
78
|
self
|
|
40
79
|
end
|
|
41
80
|
|
|
42
|
-
#
|
|
81
|
+
# Returns the API key for compatibility with old SQA.av.key usage.
|
|
82
|
+
#
|
|
83
|
+
# @return [String] The API key
|
|
84
|
+
# @raise [SQA::ConfigurationError] If no API key is set
|
|
43
85
|
def key
|
|
44
86
|
av_api_key
|
|
45
87
|
end
|
|
46
88
|
|
|
47
|
-
|
|
48
|
-
|
|
89
|
+
# Returns whether debug mode is enabled.
|
|
90
|
+
# @return [Boolean] true if debug mode is on
|
|
91
|
+
def debug?() = @config&.debug?
|
|
92
|
+
|
|
93
|
+
# Returns whether verbose mode is enabled.
|
|
94
|
+
# @return [Boolean] true if verbose mode is on
|
|
95
|
+
def verbose?() = @config&.verbose?
|
|
49
96
|
|
|
97
|
+
# Expands ~ to user's home directory in filepath.
|
|
98
|
+
#
|
|
99
|
+
# @param filepath [String] Path potentially containing ~
|
|
100
|
+
# @return [String] Path with ~ expanded
|
|
50
101
|
def homify(filepath) = filepath.gsub(/^~/, Nenv.home)
|
|
102
|
+
|
|
103
|
+
# Returns the data directory as a Pathname.
|
|
104
|
+
#
|
|
105
|
+
# @return [Pathname] Data directory path
|
|
51
106
|
def data_dir() = Pathname.new(config.data_dir)
|
|
52
|
-
def config() = @@config
|
|
53
107
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
108
|
+
# Returns the current configuration.
|
|
109
|
+
#
|
|
110
|
+
# @return [SQA::Config] Configuration instance
|
|
111
|
+
def config() = @config
|
|
57
112
|
end
|
|
58
113
|
end
|
data/lib/sqa/pattern_matcher.rb
CHANGED
|
@@ -214,13 +214,13 @@ module SQA
|
|
|
214
214
|
def pattern_quality(pattern)
|
|
215
215
|
return nil if pattern.size < 3
|
|
216
216
|
|
|
217
|
-
# Trend strength
|
|
218
|
-
first = pattern.first
|
|
219
|
-
last = pattern.last
|
|
217
|
+
# Trend strength (convert to float to avoid integer division)
|
|
218
|
+
first = pattern.first.to_f
|
|
219
|
+
last = pattern.last.to_f
|
|
220
220
|
trend = (last - first) / first
|
|
221
221
|
|
|
222
222
|
# Volatility
|
|
223
|
-
returns = pattern.each_cons(2).map { |a, b| (b - a) / a }
|
|
223
|
+
returns = pattern.each_cons(2).map { |a, b| (b - a).to_f / a }
|
|
224
224
|
volatility = standard_deviation(returns)
|
|
225
225
|
|
|
226
226
|
# Smoothness (how linear is the trend?)
|
data/lib/sqa/portfolio.rb
CHANGED
|
@@ -5,7 +5,7 @@ require 'date'
|
|
|
5
5
|
require 'csv'
|
|
6
6
|
|
|
7
7
|
class SQA::Portfolio
|
|
8
|
-
attr_accessor :positions, :trades, :cash, :initial_cash
|
|
8
|
+
attr_accessor :positions, :trades, :cash, :initial_cash, :commission
|
|
9
9
|
|
|
10
10
|
# Represents a single position in the portfolio
|
|
11
11
|
Position = Struct.new(:ticker, :shares, :avg_cost, :total_cost) do
|
data/lib/sqa/sector_analyzer.rb
CHANGED
|
@@ -89,15 +89,11 @@ module SQA
|
|
|
89
89
|
kb = @blackboards[sector]
|
|
90
90
|
all_patterns = []
|
|
91
91
|
|
|
92
|
-
|
|
93
|
-
puts "Discovering patterns for #{sector.to_s.upcase} sector"
|
|
94
|
-
puts "Analyzing #{stocks.size} stocks: #{stocks.map(&:ticker).join(', ')}"
|
|
95
|
-
puts "=" * 70
|
|
96
|
-
puts
|
|
92
|
+
debug_me {"Discovering patterns for #{sector.to_s.upcase} sector - #{stocks.size} stocks"}
|
|
97
93
|
|
|
98
94
|
# Discover patterns for each stock
|
|
99
95
|
stocks.each do |stock|
|
|
100
|
-
|
|
96
|
+
debug_me {"Analyzing #{stock.ticker}..."}
|
|
101
97
|
|
|
102
98
|
generator = SQA::StrategyGenerator.new(stock: stock, **options)
|
|
103
99
|
patterns = generator.discover_patterns
|
|
@@ -136,11 +132,7 @@ module SQA
|
|
|
136
132
|
})
|
|
137
133
|
end
|
|
138
134
|
|
|
139
|
-
|
|
140
|
-
puts "Sector Analysis Complete"
|
|
141
|
-
puts " Individual patterns found: #{all_patterns.size}"
|
|
142
|
-
puts " Sector-wide patterns: #{sector_patterns.size}"
|
|
143
|
-
puts "=" * 70
|
|
135
|
+
debug_me {"Sector Analysis Complete - #{all_patterns.size} individual, #{sector_patterns.size} sector-wide patterns"}
|
|
144
136
|
|
|
145
137
|
sector_patterns
|
|
146
138
|
end
|
data/lib/sqa/stock.rb
CHANGED
|
@@ -1,12 +1,83 @@
|
|
|
1
1
|
# lib/sqa/stock.rb
|
|
2
2
|
|
|
3
|
+
# Represents a stock with price history, metadata, and technical analysis capabilities.
|
|
4
|
+
# This is the primary domain object for interacting with stock data.
|
|
5
|
+
#
|
|
6
|
+
# @example Basic usage
|
|
7
|
+
# stock = SQA::Stock.new(ticker: 'AAPL')
|
|
8
|
+
# prices = stock.df["adj_close_price"].to_a
|
|
9
|
+
# puts stock.to_s
|
|
10
|
+
#
|
|
11
|
+
# @example With different data source
|
|
12
|
+
# stock = SQA::Stock.new(ticker: 'MSFT', source: :yahoo_finance)
|
|
13
|
+
#
|
|
3
14
|
class SQA::Stock
|
|
4
15
|
extend Forwardable
|
|
5
16
|
|
|
6
|
-
|
|
17
|
+
# Default Alpha Vantage API URL
|
|
18
|
+
# @return [String] The base URL for Alpha Vantage API
|
|
19
|
+
ALPHA_VANTAGE_URL = "https://www.alphavantage.co".freeze
|
|
7
20
|
|
|
21
|
+
# @deprecated Use {.connection} method instead. Will be removed in v1.0.0
|
|
22
|
+
# @return [Faraday::Connection] Legacy constant for backward compatibility
|
|
23
|
+
CONNECTION = Faraday.new(url: ALPHA_VANTAGE_URL)
|
|
24
|
+
|
|
25
|
+
class << self
|
|
26
|
+
# Returns the current Faraday connection for API requests.
|
|
27
|
+
# Allows injection of custom connections for testing or different configurations.
|
|
28
|
+
#
|
|
29
|
+
# @return [Faraday::Connection] The current connection instance
|
|
30
|
+
def connection
|
|
31
|
+
@connection ||= default_connection
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Sets a custom Faraday connection.
|
|
35
|
+
# Useful for testing with mocks/stubs or configuring different API endpoints.
|
|
36
|
+
#
|
|
37
|
+
# @param conn [Faraday::Connection] Custom Faraday connection to use
|
|
38
|
+
# @return [Faraday::Connection] The connection that was set
|
|
39
|
+
def connection=(conn)
|
|
40
|
+
@connection = conn
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Creates the default Faraday connection to Alpha Vantage.
|
|
44
|
+
#
|
|
45
|
+
# @return [Faraday::Connection] A new connection to Alpha Vantage API
|
|
46
|
+
def default_connection
|
|
47
|
+
Faraday.new(url: ALPHA_VANTAGE_URL)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Resets the connection to default.
|
|
51
|
+
# Useful for testing cleanup to ensure fresh state between tests.
|
|
52
|
+
#
|
|
53
|
+
# @return [nil]
|
|
54
|
+
def reset_connection!
|
|
55
|
+
@connection = nil
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# @!attribute [rw] data
|
|
60
|
+
# @return [SQA::DataFrame::Data] Stock metadata (ticker, name, exchange, etc.)
|
|
61
|
+
# @!attribute [rw] df
|
|
62
|
+
# @return [SQA::DataFrame] Price and volume data as a DataFrame
|
|
63
|
+
# @!attribute [rw] klass
|
|
64
|
+
# @return [Class] The data source class (e.g., SQA::DataFrame::AlphaVantage)
|
|
65
|
+
# @!attribute [rw] transformers
|
|
66
|
+
# @return [Hash] Column transformers for data normalization
|
|
67
|
+
# @!attribute [rw] strategy
|
|
68
|
+
# @return [SQA::Strategy, nil] Optional trading strategy attached to this stock
|
|
8
69
|
attr_accessor :data, :df, :klass, :transformers, :strategy
|
|
9
70
|
|
|
71
|
+
# Creates a new Stock instance and loads or fetches its data.
|
|
72
|
+
#
|
|
73
|
+
# @param ticker [String] The stock ticker symbol (e.g., 'AAPL', 'MSFT')
|
|
74
|
+
# @param source [Symbol] The data source to use (:alpha_vantage or :yahoo_finance)
|
|
75
|
+
# @raise [SQA::DataFetchError] If data cannot be fetched and no cached data exists
|
|
76
|
+
#
|
|
77
|
+
# @example
|
|
78
|
+
# stock = SQA::Stock.new(ticker: 'AAPL')
|
|
79
|
+
# stock = SQA::Stock.new(ticker: 'GOOG', source: :yahoo_finance)
|
|
80
|
+
#
|
|
10
81
|
def initialize(ticker:, source: :alpha_vantage)
|
|
11
82
|
@ticker = ticker.downcase
|
|
12
83
|
@source = source
|
|
@@ -25,9 +96,14 @@ class SQA::Stock
|
|
|
25
96
|
@transformers = "SQA::DataFrame::#{@source.to_s.camelize}::TRANSFORMERS".constantize
|
|
26
97
|
|
|
27
98
|
load_or_create_data
|
|
28
|
-
|
|
99
|
+
update_dataframe
|
|
29
100
|
end
|
|
30
101
|
|
|
102
|
+
# Loads existing data from cache or creates new data structure.
|
|
103
|
+
# If cached data exists, loads from JSON file. Otherwise creates
|
|
104
|
+
# minimal structure and attempts to fetch overview from API.
|
|
105
|
+
#
|
|
106
|
+
# @return [void]
|
|
31
107
|
def load_or_create_data
|
|
32
108
|
if @data_path.exist?
|
|
33
109
|
@data = SQA::DataFrame::Data.new(JSON.parse(@data_path.read))
|
|
@@ -44,27 +120,57 @@ class SQA::Stock
|
|
|
44
120
|
end
|
|
45
121
|
end
|
|
46
122
|
|
|
123
|
+
# Creates a new minimal data structure for the stock.
|
|
124
|
+
#
|
|
125
|
+
# @return [SQA::DataFrame::Data] The newly created data object
|
|
47
126
|
def create_data
|
|
48
|
-
@data = SQA::DataFrame::Data.new(ticker: @ticker, source: @source, indicators: {
|
|
127
|
+
@data = SQA::DataFrame::Data.new(ticker: @ticker, source: @source, indicators: {})
|
|
49
128
|
end
|
|
50
129
|
|
|
130
|
+
# Updates the stock's overview data from the API.
|
|
131
|
+
# Silently handles errors since overview data is optional.
|
|
132
|
+
#
|
|
133
|
+
# @return [void]
|
|
51
134
|
def update
|
|
52
135
|
begin
|
|
53
136
|
merge_overview
|
|
54
|
-
rescue => e
|
|
137
|
+
rescue StandardError => e
|
|
55
138
|
# Log warning but don't fail - overview data is optional
|
|
56
139
|
# Common causes: rate limits, network issues, API errors
|
|
57
140
|
warn "Warning: Could not fetch overview data for #{@ticker} (#{e.class}: #{e.message}). Continuing without it."
|
|
58
141
|
end
|
|
59
142
|
end
|
|
60
143
|
|
|
144
|
+
# Persists the stock's metadata to a JSON file.
|
|
145
|
+
#
|
|
146
|
+
# @return [Integer] Number of bytes written
|
|
61
147
|
def save_data
|
|
62
148
|
@data_path.write(@data.to_json)
|
|
63
149
|
end
|
|
64
150
|
|
|
151
|
+
# @!method ticker
|
|
152
|
+
# @return [String] The stock's ticker symbol
|
|
153
|
+
# @!method name
|
|
154
|
+
# @return [String, nil] The company name
|
|
155
|
+
# @!method exchange
|
|
156
|
+
# @return [String, nil] The exchange where the stock trades
|
|
157
|
+
# @!method source
|
|
158
|
+
# @return [Symbol] The data source (:alpha_vantage or :yahoo_finance)
|
|
159
|
+
# @!method indicators
|
|
160
|
+
# @return [Hash] Cached indicator values
|
|
161
|
+
# @!method indicators=(value)
|
|
162
|
+
# @param value [Hash] New indicator values
|
|
163
|
+
# @!method overview
|
|
164
|
+
# @return [Hash, nil] Company overview data from API
|
|
65
165
|
def_delegators :@data, :ticker, :name, :exchange, :source, :indicators, :indicators=, :overview
|
|
66
166
|
|
|
67
|
-
|
|
167
|
+
# Updates the DataFrame with price data.
|
|
168
|
+
# Loads from cache if available, otherwise fetches from API.
|
|
169
|
+
# Applies migrations for old data formats and updates with recent data.
|
|
170
|
+
#
|
|
171
|
+
# @return [void]
|
|
172
|
+
# @raise [SQA::DataFetchError] If data cannot be fetched and no cache exists
|
|
173
|
+
def update_dataframe
|
|
68
174
|
if @df_path.exist?
|
|
69
175
|
# Load cached CSV - transformers already applied when data was first fetched
|
|
70
176
|
# Don't reapply them as columns are already in correct format
|
|
@@ -103,15 +209,22 @@ class SQA::Stock
|
|
|
103
209
|
@df = @klass.recent(@ticker, full: true)
|
|
104
210
|
@df.to_csv(@df_path)
|
|
105
211
|
return
|
|
106
|
-
rescue => e
|
|
212
|
+
rescue StandardError => e
|
|
107
213
|
# If we can't fetch data, raise a more helpful error
|
|
108
|
-
raise
|
|
214
|
+
raise SQA::DataFetchError.new(
|
|
215
|
+
"Unable to fetch data for #{@ticker}. Please ensure API key is set or provide cached CSV file at #{@df_path}. Error: #{e.message}",
|
|
216
|
+
original: e
|
|
217
|
+
)
|
|
109
218
|
end
|
|
110
219
|
end
|
|
111
220
|
|
|
112
221
|
update_dataframe_with_recent_data
|
|
113
222
|
end
|
|
114
223
|
|
|
224
|
+
# Fetches recent data from API and appends to existing DataFrame.
|
|
225
|
+
# Only called if should_update? returns true.
|
|
226
|
+
#
|
|
227
|
+
# @return [void]
|
|
115
228
|
def update_dataframe_with_recent_data
|
|
116
229
|
return unless should_update?
|
|
117
230
|
|
|
@@ -125,13 +238,25 @@ class SQA::Stock
|
|
|
125
238
|
@df.concat_and_deduplicate!(df2)
|
|
126
239
|
@df.to_csv(@df_path)
|
|
127
240
|
end
|
|
128
|
-
rescue => e
|
|
241
|
+
rescue StandardError => e
|
|
129
242
|
# Log warning but don't fail - we have cached data
|
|
130
243
|
# Common causes: rate limits, network issues, API errors
|
|
131
244
|
warn "Warning: Could not update #{@ticker} from API (#{e.class}: #{e.message}). Using cached data."
|
|
132
245
|
end
|
|
133
246
|
end
|
|
134
247
|
|
|
248
|
+
# @deprecated Use {#update_dataframe} instead. Will be removed in v1.0.0
|
|
249
|
+
# @return [void]
|
|
250
|
+
def update_the_dataframe
|
|
251
|
+
warn "[SQA DEPRECATION] update_the_dataframe is deprecated; use update_dataframe instead" if $VERBOSE
|
|
252
|
+
update_dataframe
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Determines whether the DataFrame should be updated from the API.
|
|
256
|
+
# Returns false if lazy_update is enabled, API key is missing,
|
|
257
|
+
# or data is already current.
|
|
258
|
+
#
|
|
259
|
+
# @return [Boolean] true if update should proceed, false otherwise
|
|
135
260
|
def should_update?
|
|
136
261
|
# Don't update if we're in lazy update mode
|
|
137
262
|
return false if SQA.config.lazy_update
|
|
@@ -140,7 +265,7 @@ class SQA::Stock
|
|
|
140
265
|
if @source == :alpha_vantage
|
|
141
266
|
begin
|
|
142
267
|
SQA.av_api_key
|
|
143
|
-
rescue
|
|
268
|
+
rescue SQA::ConfigurationError
|
|
144
269
|
return false
|
|
145
270
|
end
|
|
146
271
|
end
|
|
@@ -151,7 +276,7 @@ class SQA::Stock
|
|
|
151
276
|
begin
|
|
152
277
|
last_timestamp = Date.parse(@df["timestamp"].to_a.last)
|
|
153
278
|
return false if last_timestamp >= Date.today
|
|
154
|
-
rescue => e
|
|
279
|
+
rescue ArgumentError, Date::Error => e
|
|
155
280
|
# If we can't parse the date, assume we need to update
|
|
156
281
|
warn "Warning: Could not parse last timestamp for #{@ticker} (#{e.message}). Will attempt update." if $VERBOSE
|
|
157
282
|
end
|
|
@@ -160,6 +285,12 @@ class SQA::Stock
|
|
|
160
285
|
true
|
|
161
286
|
end
|
|
162
287
|
|
|
288
|
+
# Returns a human-readable string representation of the stock.
|
|
289
|
+
#
|
|
290
|
+
# @return [String] Summary including ticker, data points count, and date range
|
|
291
|
+
#
|
|
292
|
+
# @example
|
|
293
|
+
# stock.to_s # => "aapl with 252 data points from 2023-01-03 to 2023-12-29"
|
|
163
294
|
def to_s
|
|
164
295
|
"#{ticker} with #{@df.size} data points from #{@df["timestamp"].to_a.first} to #{@df["timestamp"].to_a.last}"
|
|
165
296
|
end
|
|
@@ -167,13 +298,18 @@ class SQA::Stock
|
|
|
167
298
|
# This ensures compatibility with TA-Lib indicators which expect arrays in this order
|
|
168
299
|
alias_method :inspect, :to_s
|
|
169
300
|
|
|
301
|
+
# Fetches and merges company overview data from Alpha Vantage API.
|
|
302
|
+
# Converts API response keys to snake_case and appropriate data types.
|
|
303
|
+
#
|
|
304
|
+
# @return [Hash] The merged overview data
|
|
305
|
+
# @raise [ApiError] If the API returns an error response
|
|
170
306
|
def merge_overview
|
|
171
307
|
temp = JSON.parse(
|
|
172
|
-
|
|
308
|
+
self.class.connection.get("/query?function=OVERVIEW&symbol=#{ticker.upcase}&apikey=#{SQA.av.key}")
|
|
173
309
|
.to_hash[:body]
|
|
174
310
|
)
|
|
175
311
|
|
|
176
|
-
if temp.
|
|
312
|
+
if temp.key?("Information")
|
|
177
313
|
ApiError.raise(temp["Information"])
|
|
178
314
|
end
|
|
179
315
|
|
|
@@ -192,10 +328,20 @@ class SQA::Stock
|
|
|
192
328
|
## Class Methods
|
|
193
329
|
|
|
194
330
|
class << self
|
|
331
|
+
# Fetches top gainers, losers, and most actively traded stocks from Alpha Vantage.
|
|
332
|
+
# Results are cached after the first call.
|
|
333
|
+
#
|
|
334
|
+
# @return [Hashie::Mash] Object with top_gainers, top_losers, and most_actively_traded arrays
|
|
335
|
+
#
|
|
336
|
+
# @example
|
|
337
|
+
# top = SQA::Stock.top
|
|
338
|
+
# top.top_gainers.each { |stock| puts "#{stock.ticker}: +#{stock.change_percentage}%" }
|
|
339
|
+
# top.top_losers.first.ticker # => "XYZ"
|
|
340
|
+
#
|
|
195
341
|
def top
|
|
196
|
-
return
|
|
342
|
+
return @top if @top
|
|
197
343
|
|
|
198
|
-
a_hash = JSON.parse(
|
|
344
|
+
a_hash = JSON.parse(connection.get("/query?function=TOP_GAINERS_LOSERS&apikey=#{SQA.av.key}").to_hash[:body])
|
|
199
345
|
|
|
200
346
|
mash = Hashie::Mash.new(a_hash)
|
|
201
347
|
|
|
@@ -216,7 +362,15 @@ class SQA::Stock
|
|
|
216
362
|
end
|
|
217
363
|
end
|
|
218
364
|
|
|
219
|
-
|
|
365
|
+
@top = mash
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# Resets the cached top gainers/losers data.
|
|
369
|
+
# Useful for testing or forcing a refresh.
|
|
370
|
+
#
|
|
371
|
+
# @return [nil]
|
|
372
|
+
def reset_top!
|
|
373
|
+
@top = nil
|
|
220
374
|
end
|
|
221
375
|
end
|
|
222
376
|
end
|
data/lib/sqa/strategy.rb
CHANGED
|
@@ -1,16 +1,45 @@
|
|
|
1
1
|
# lib/sqa/strategy.rb
|
|
2
2
|
|
|
3
|
+
# Framework for managing and executing trading strategies.
|
|
4
|
+
# Strategies are pluggable modules that generate buy/sell/hold signals.
|
|
5
|
+
#
|
|
6
|
+
# @example Basic usage
|
|
7
|
+
# strategy = SQA::Strategy.new
|
|
8
|
+
# strategy.add(SQA::Strategy::RSI)
|
|
9
|
+
# strategy.add(SQA::Strategy::MACD)
|
|
10
|
+
# signals = strategy.execute(vector) # => [:buy, :hold]
|
|
11
|
+
#
|
|
12
|
+
# @example Auto-loading strategies
|
|
13
|
+
# strategy = SQA::Strategy.new
|
|
14
|
+
# strategy.auto_load(except: [:random])
|
|
15
|
+
#
|
|
3
16
|
class SQA::Strategy
|
|
17
|
+
# @!attribute [rw] strategies
|
|
18
|
+
# @return [Array<Method>] Collection of strategy trade methods
|
|
4
19
|
attr_accessor :strategies
|
|
5
20
|
|
|
21
|
+
# Creates a new Strategy instance with an empty strategies collection.
|
|
6
22
|
def initialize
|
|
7
23
|
@strategies = []
|
|
8
24
|
end
|
|
9
25
|
|
|
26
|
+
# Adds a trading strategy to the collection.
|
|
27
|
+
# Strategies must be either a Class with a .trade method or a Method object.
|
|
28
|
+
#
|
|
29
|
+
# @param a_strategy [Class, Method] Strategy to add
|
|
30
|
+
# @return [Array<Method>] Updated strategies collection
|
|
31
|
+
# @raise [BadParameterError] If strategy is not a Class or Method
|
|
32
|
+
#
|
|
33
|
+
# @example Adding a class-based strategy
|
|
34
|
+
# strategy.add(SQA::Strategy::RSI)
|
|
35
|
+
#
|
|
36
|
+
# @example Adding a method directly
|
|
37
|
+
# strategy.add(MyModule.method(:custom_trade))
|
|
38
|
+
#
|
|
10
39
|
def add(a_strategy)
|
|
11
|
-
raise BadParameterError unless
|
|
40
|
+
raise BadParameterError unless a_strategy.is_a?(Class) || a_strategy.is_a?(Method)
|
|
12
41
|
|
|
13
|
-
a_proc = if
|
|
42
|
+
a_proc = if a_strategy.is_a?(Class)
|
|
14
43
|
a_strategy.method(:trade)
|
|
15
44
|
else
|
|
16
45
|
a_strategy
|
|
@@ -19,13 +48,34 @@ class SQA::Strategy
|
|
|
19
48
|
@strategies << a_proc
|
|
20
49
|
end
|
|
21
50
|
|
|
51
|
+
# Executes all registered strategies with the given data vector.
|
|
52
|
+
#
|
|
53
|
+
# @param v [OpenStruct] Data vector containing indicator values and prices
|
|
54
|
+
# @return [Array<Symbol>] Array of signals (:buy, :sell, or :hold) from each strategy
|
|
55
|
+
#
|
|
56
|
+
# @example
|
|
57
|
+
# vector = OpenStruct.new(rsi: 25, prices: prices_array)
|
|
58
|
+
# signals = strategy.execute(vector) # => [:buy, :hold, :sell]
|
|
59
|
+
#
|
|
22
60
|
def execute(v)
|
|
23
61
|
result = []
|
|
24
|
-
#
|
|
62
|
+
# NOTE: Could be parallelized with Parallel gem for large strategy sets
|
|
25
63
|
@strategies.each { |signal| result << signal.call(v) }
|
|
26
64
|
result
|
|
27
65
|
end
|
|
28
66
|
|
|
67
|
+
# Auto-loads strategy files from the strategy directory.
|
|
68
|
+
#
|
|
69
|
+
# @param except [Array<Symbol>] Strategy names to exclude (default: [:common])
|
|
70
|
+
# @param only [Array<Symbol>] If provided, only load these strategies
|
|
71
|
+
# @return [nil]
|
|
72
|
+
#
|
|
73
|
+
# @example Load all except random
|
|
74
|
+
# strategy.auto_load(except: [:common, :random])
|
|
75
|
+
#
|
|
76
|
+
# @example Load only specific strategies
|
|
77
|
+
# strategy.auto_load(only: [:rsi, :macd])
|
|
78
|
+
#
|
|
29
79
|
def auto_load(except: [:common], only: [])
|
|
30
80
|
dir_path = Pathname.new(__dir__) + "strategy"
|
|
31
81
|
except = Array(except).map{|f| f.to_s.downcase}
|
|
@@ -47,9 +97,17 @@ class SQA::Strategy
|
|
|
47
97
|
nil
|
|
48
98
|
end
|
|
49
99
|
|
|
100
|
+
# Returns all available strategy classes in the SQA::Strategy namespace.
|
|
101
|
+
#
|
|
102
|
+
# @return [Array<Class>] Array of strategy classes
|
|
103
|
+
#
|
|
104
|
+
# @example
|
|
105
|
+
# SQA::Strategy.new.available
|
|
106
|
+
# # => [SQA::Strategy::RSI, SQA::Strategy::MACD, ...]
|
|
107
|
+
#
|
|
50
108
|
def available
|
|
51
109
|
ObjectSpace.each_object(Class).select { |klass|
|
|
52
|
-
klass.
|
|
110
|
+
klass.name&.start_with?("SQA::Strategy::")
|
|
53
111
|
}
|
|
54
112
|
end
|
|
55
113
|
end
|