Class: SQA::Stock
Overview
Represents a stock with price history, metadata, and technical analysis capabilities. This is the primary domain object for interacting with stock data.
Constant Summary collapse
- ALPHA_VANTAGE_URL =
Default Alpha Vantage API URL
"https://www.alphavantage.co".freeze
- CONNECTION =
Deprecated.
Use connection method instead. Will be removed in v1.0.0
Returns Legacy constant for backward compatibility.
Faraday.new(url: ALPHA_VANTAGE_URL)
Instance Attribute Summary collapse
-
#data ⇒ SQA::DataFrame::Data
Stock metadata (ticker, name, exchange, etc.).
-
#df ⇒ SQA::DataFrame
Price and volume data as a DataFrame.
-
#klass ⇒ Class
The data source class (e.g., SQA::DataFrame::AlphaVantage).
-
#strategy ⇒ SQA::Strategy?
Optional trading strategy attached to this stock.
-
#transformers ⇒ Hash
Column transformers for data normalization.
Class Method Summary collapse
-
.connection ⇒ Faraday::Connection
Returns the current Faraday connection for API requests.
-
.connection=(conn) ⇒ Faraday::Connection
Sets a custom Faraday connection.
-
.default_connection ⇒ Faraday::Connection
Creates the default Faraday connection to Alpha Vantage.
-
.reset_connection! ⇒ nil
Resets the connection to default.
-
.reset_top! ⇒ nil
Resets the cached top gainers/losers data.
-
.top ⇒ Hashie::Mash
Fetches top gainers, losers, and most actively traded stocks from Alpha Vantage.
Instance Method Summary collapse
-
#create_data ⇒ SQA::DataFrame::Data
Creates a new minimal data structure for the stock.
-
#exchange ⇒ String?
The exchange where the stock trades.
-
#indicators ⇒ Hash
Cached indicator values.
- #indicators=(value) ⇒ Object
-
#initialize(ticker:, source: :alpha_vantage) ⇒ Stock
constructor
Creates a new Stock instance and loads or fetches its data.
-
#load_or_create_data ⇒ void
Loads existing data from cache or creates new data structure.
-
#merge_overview ⇒ Hash
Fetches and merges company overview data from Alpha Vantage API.
-
#name ⇒ String?
The company name.
-
#overview ⇒ Hash?
Company overview data from API.
-
#save_data ⇒ Integer
Persists the stock’s metadata to a JSON file.
-
#should_update? ⇒ Boolean
Determines whether the DataFrame should be updated from the API.
-
#source ⇒ Symbol
The data source (:alpha_vantage or :yahoo_finance).
-
#ticker ⇒ String
The stock’s ticker symbol.
-
#to_s ⇒ String
(also: #inspect)
Returns a human-readable string representation of the stock.
-
#update ⇒ void
Updates the stock’s overview data from the API.
-
#update_dataframe ⇒ void
Updates the DataFrame with price data.
-
#update_dataframe_with_recent_data ⇒ void
Fetches recent data from API and appends to existing DataFrame.
-
#update_the_dataframe ⇒ void
deprecated
Deprecated.
Use #update_dataframe instead. Will be removed in v1.0.0
Constructor Details
#initialize(ticker:, source: :alpha_vantage) ⇒ Stock
Creates a new Stock instance and loads or fetches its data.
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
# File 'lib/sqa/stock.rb', line 81 def initialize(ticker:, source: :alpha_vantage) @ticker = ticker.downcase @source = source @data_path = SQA.data_dir + "#{@ticker}.json" @df_path = SQA.data_dir + "#{@ticker}.csv" # Validate ticker if validation data is available and cached data doesn't exist unless @data_path.exist? && @df_path.exist? unless SQA::Ticker.valid?(ticker) warn "Warning: Ticker #{ticker} could not be validated. Proceeding anyway." if $VERBOSE end end @klass = "SQA::DataFrame::#{@source.to_s.camelize}".constantize @transformers = "SQA::DataFrame::#{@source.to_s.camelize}::TRANSFORMERS".constantize load_or_create_data update_dataframe end |
Instance Attribute Details
#data ⇒ SQA::DataFrame::Data
Returns Stock metadata (ticker, name, exchange, etc.).
69 70 71 |
# File 'lib/sqa/stock.rb', line 69 def data @data end |
#df ⇒ SQA::DataFrame
Returns Price and volume data as a DataFrame.
69 |
# File 'lib/sqa/stock.rb', line 69 attr_accessor :data, :df, :klass, :transformers, :strategy |
#klass ⇒ Class
Returns The data source class (e.g., SQA::DataFrame::AlphaVantage).
69 |
# File 'lib/sqa/stock.rb', line 69 attr_accessor :data, :df, :klass, :transformers, :strategy |
#strategy ⇒ SQA::Strategy?
Returns Optional trading strategy attached to this stock.
69 |
# File 'lib/sqa/stock.rb', line 69 attr_accessor :data, :df, :klass, :transformers, :strategy |
#transformers ⇒ Hash
Returns Column transformers for data normalization.
69 |
# File 'lib/sqa/stock.rb', line 69 attr_accessor :data, :df, :klass, :transformers, :strategy |
Class Method Details
.connection ⇒ Faraday::Connection
Returns the current Faraday connection for API requests. Allows injection of custom connections for testing or different configurations.
30 31 32 |
# File 'lib/sqa/stock.rb', line 30 def connection @connection ||= default_connection end |
.connection=(conn) ⇒ Faraday::Connection
Sets a custom Faraday connection. Useful for testing with mocks/stubs or configuring different API endpoints.
39 40 41 |
# File 'lib/sqa/stock.rb', line 39 def connection=(conn) @connection = conn end |
.default_connection ⇒ Faraday::Connection
Creates the default Faraday connection to Alpha Vantage.
46 47 48 |
# File 'lib/sqa/stock.rb', line 46 def default_connection Faraday.new(url: ALPHA_VANTAGE_URL) end |
.reset_connection! ⇒ nil
Resets the connection to default. Useful for testing cleanup to ensure fresh state between tests.
54 55 56 |
# File 'lib/sqa/stock.rb', line 54 def reset_connection! @connection = nil end |
.reset_top! ⇒ nil
Resets the cached top gainers/losers data. Useful for testing or forcing a refresh.
383 384 385 |
# File 'lib/sqa/stock.rb', line 383 def reset_top! @top = nil end |
.top ⇒ Hashie::Mash
Fetches top gainers, losers, and most actively traded stocks from Alpha Vantage. Results are cached after the first call.
352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 |
# File 'lib/sqa/stock.rb', line 352 def top return @top if @top a_hash = JSON.parse(connection.get("/query?function=TOP_GAINERS_LOSERS&apikey=#{SQA.av.key}").to_hash[:body]) mash = Hashie::Mash.new(a_hash) keys = mash.top_gainers.first.keys %w[top_gainers top_losers most_actively_traded].each do |collection| mash.send(collection).each do |e| keys.each do |k| case k when 'ticker' # Leave it as a String when 'volume' e[k] = e[k].to_i else e[k] = e[k].to_f end end end end @top = mash end |
Instance Method Details
#create_data ⇒ SQA::DataFrame::Data
Creates a new minimal data structure for the stock.
126 127 128 |
# File 'lib/sqa/stock.rb', line 126 def create_data @data = SQA::DataFrame::Data.new(ticker: @ticker, source: @source, indicators: {}) end |
#exchange ⇒ String?
Returns The exchange where the stock trades.
176 |
# File 'lib/sqa/stock.rb', line 176 def_delegators :@data, :ticker, :name, :exchange, :source, :indicators, :indicators=, :overview |
#indicators ⇒ Hash
Returns Cached indicator values.
176 |
# File 'lib/sqa/stock.rb', line 176 def_delegators :@data, :ticker, :name, :exchange, :source, :indicators, :indicators=, :overview |
#indicators=(value) ⇒ Object
176 |
# File 'lib/sqa/stock.rb', line 176 def_delegators :@data, :ticker, :name, :exchange, :source, :indicators, :indicators=, :overview |
#load_or_create_data ⇒ void
This method returns an undefined value.
Loads existing data from cache or creates new data structure. If cached data exists, loads from JSON file. Otherwise creates minimal structure and attempts to fetch overview from API.
107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
# File 'lib/sqa/stock.rb', line 107 def load_or_create_data if @data_path.exist? @data = SQA::DataFrame::Data.new(JSON.parse(@data_path.read)) else # Create minimal data structure create_data # Try to fetch overview data, but don't fail if we can't # This is optional metadata - we can work with just price data update # Save whatever data we have (even if overview fetch failed) save_data end end |
#merge_overview ⇒ Hash
Fetches and merges company overview data from Alpha Vantage API. Converts API response keys to snake_case and appropriate data types.
317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 |
# File 'lib/sqa/stock.rb', line 317 def merge_overview temp = JSON.parse( self.class.connection.get("/query?function=OVERVIEW&symbol=#{ticker.upcase}&apikey=#{SQA.av.key}") .to_hash[:body] ) if temp.key?("Information") ApiError.raise(temp["Information"]) end temp2 = {} string_values = %w[address asset_type cik country currency description dividend_date ex_dividend_date exchange fiscal_year_end industry latest_quarter name sector symbol] temp.keys.each do |k| new_k = k.underscore temp2[new_k] = string_values.include?(new_k) ? temp[k] : temp[k].to_f end @data.overview = temp2 end |
#name ⇒ String?
Returns The company name.
176 |
# File 'lib/sqa/stock.rb', line 176 def_delegators :@data, :ticker, :name, :exchange, :source, :indicators, :indicators=, :overview |
#overview ⇒ Hash?
Returns Company overview data from API.
176 |
# File 'lib/sqa/stock.rb', line 176 def_delegators :@data, :ticker, :name, :exchange, :source, :indicators, :indicators=, :overview |
#save_data ⇒ Integer
Persists the stock’s metadata to a JSON file.
158 159 160 |
# File 'lib/sqa/stock.rb', line 158 def save_data @data_path.write(@data.to_json) end |
#should_update? ⇒ Boolean
Determines whether the DataFrame should be updated from the API. Returns false if lazy_update is enabled, API key is missing, or data is already current.
271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 |
# File 'lib/sqa/stock.rb', line 271 def should_update? # Don't update if we're in lazy update mode return false if SQA.config.lazy_update # Don't update if we don't have an API key (only relevant for Alpha Vantage) if @source == :alpha_vantage begin SQA.av_api_key rescue SQA::ConfigurationError return false end end # Don't update if CSV data is already current (last timestamp is today or later) # This prevents unnecessary API calls when we already have today's data if @df && @df.size > 0 begin = Date.parse(@df["timestamp"].to_a.last) return false if >= Date.today rescue ArgumentError, Date::Error => e # If we can't parse the date, assume we need to update warn "Warning: Could not parse last timestamp for #{@ticker} (#{e.message}). Will attempt update." if $VERBOSE end end true end |
#source ⇒ Symbol
Returns The data source (:alpha_vantage or :yahoo_finance).
176 |
# File 'lib/sqa/stock.rb', line 176 def_delegators :@data, :ticker, :name, :exchange, :source, :indicators, :indicators=, :overview |
#ticker ⇒ String
Returns The stock’s ticker symbol.
176 |
# File 'lib/sqa/stock.rb', line 176 def_delegators :@data, :ticker, :name, :exchange, :source, :indicators, :indicators=, :overview |
#to_s ⇒ String Also known as: inspect
Returns a human-readable string representation of the stock.
305 306 307 |
# File 'lib/sqa/stock.rb', line 305 def to_s "#{ticker} with #{@df.size} data points from #{@df["timestamp"].to_a.first} to #{@df["timestamp"].to_a.last}" end |
#update ⇒ void
This method returns an undefined value.
Updates the stock’s overview data from the API. Silently handles errors since overview data is optional.
145 146 147 148 149 150 151 152 153 |
# File 'lib/sqa/stock.rb', line 145 def update begin merge_overview rescue StandardError => e # Log warning but don't fail - overview data is optional # Common causes: rate limits, network issues, API errors warn "Warning: Could not fetch overview data for #{@ticker} (#{e.class}: #{e.message}). Continuing without it." end end |
#update_dataframe ⇒ void
This method returns an undefined value.
Updates the DataFrame with price data. Loads from cache if available, otherwise fetches from API. Applies migrations for old data formats and updates with recent data.
184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 |
# File 'lib/sqa/stock.rb', line 184 def update_dataframe if @df_path.exist? # Load cached CSV - transformers already applied when data was first fetched # Don't reapply them as columns are already in correct format @df = SQA::DataFrame.load(source: @df_path) migrated = false # Migration 1: Rename old column names to new convention # Old files may have: open, high, low, close # New files should have: open_price, high_price, low_price, close_price if @df.columns.include?("open") && !@df.columns.include?("open_price") old_to_new_mapping = { "open" => "open_price", "high" => "high_price", "low" => "low_price", "close" => "close_price" } @df.rename_columns!(old_to_new_mapping) migrated = true end # Migration 2: Add adj_close_price column if missing (for old cached files) # This ensures compatibility when appending new data that includes this column unless @df.columns.include?("adj_close_price") @df.data = @df.data.with_column( @df.data["close_price"].alias("adj_close_price") ) migrated = true end # Save migrated DataFrame to avoid repeating migration @df.to_csv(@df_path) if migrated else # Fetch fresh data from source (applies transformers and mapping) begin @df = @klass.recent(@ticker, full: true) @df.to_csv(@df_path) return rescue StandardError => e # If we can't fetch data, raise a more helpful error raise SQA::DataFetchError.new( "Unable to fetch data for #{@ticker}. Please ensure API key is set or provide cached CSV file at #{@df_path}. Error: #{e.message}", original: e ) end end update_dataframe_with_recent_data end |
#update_dataframe_with_recent_data ⇒ void
This method returns an undefined value.
Fetches recent data from API and appends to existing DataFrame. Only called if should_update? returns true.
239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 |
# File 'lib/sqa/stock.rb', line 239 def update_dataframe_with_recent_data return unless should_update? begin # CSV is sorted ascending (oldest first, TA-Lib compatible), so .last gets the most recent date from_date = Date.parse(@df["timestamp"].to_a.last) df2 = @klass.recent(@ticker, from_date: from_date) if df2 && (df2.size > 0) # Use concat_and_deduplicate! to prevent duplicate timestamps and maintain ascending sort @df.concat_and_deduplicate!(df2) @df.to_csv(@df_path) end rescue StandardError => e # Log warning but don't fail - we have cached data # Common causes: rate limits, network issues, API errors warn "Warning: Could not update #{@ticker} from API (#{e.class}: #{e.message}). Using cached data." end end |
#update_the_dataframe ⇒ void
Use #update_dataframe instead. Will be removed in v1.0.0
This method returns an undefined value.
261 262 263 264 |
# File 'lib/sqa/stock.rb', line 261 def update_the_dataframe warn "[SQA DEPRECATION] update_the_dataframe is deprecated; use update_dataframe instead" if $VERBOSE update_dataframe end |