Class: SQA::DataFrame::AlphaVantage

Inherits:
Object
  • Object
show all
Defined in:
lib/sqa/data_frame/alpha_vantage.rb

Constant Summary collapse

CONNECTION =
Faraday.new(url: 'https://www.alphavantage.co')
HEADERS =
YahooFinance::HEADERS
HEADER_MAPPING =

The Alpha Vantage CSV format uses these exact column names: timestamp, open, high, low, close, volume We remap them to match Yahoo Finance column names for consistency

{
  "timestamp" => HEADERS[0],  # :timestamp (already matches, but explicit)
  "open"      => HEADERS[1],  # :open_price
  "high"      => HEADERS[2],  # :high_price
  "low"       => HEADERS[3],  # :low_price
  "close"     => HEADERS[4],  # :close_price (AND :adj_close_price - AV doesn't split these)
  "volume"    => HEADERS[6]   # :volume
}
TRANSFORMERS =

Transformers applied AFTER column renaming Alpha Vantage CSV doesn’t have adjusted_close, so we only transform what exists

{
  HEADERS[1] => -> (v) { v.to_f.round(3) },  # :open_price
  HEADERS[2] => -> (v) { v.to_f.round(3) },  # :high_price
  HEADERS[3] => -> (v) { v.to_f.round(3) },  # :low_price
  HEADERS[4] => -> (v) { v.to_f.round(3) },  # :close_price
  # HEADERS[5] - :adj_close_price doesn't exist in Alpha Vantage CSV
  HEADERS[6] => -> (v) { v.to_i }            # :volume
}

Class Method Summary collapse

Class Method Details

.recent(ticker, full: false, from_date: nil) ⇒ Object

Get recent data from Alpha Vantage API

ticker String the security to retrieve full Boolean whether to fetch full history or compact (last 100 days) from_date Date optional date to fetch data after (for incremental updates)

Returns: SQA::DataFrame sorted in ASCENDING order (oldest to newest) Note: Alpha Vantage returns data newest-first, but we sort ascending for TA-Lib compatibility



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/sqa/data_frame/alpha_vantage.rb', line 46

def self.recent(ticker, full: false, from_date: nil)
  response  = CONNECTION.get(
    "/query?" +
    "function=TIME_SERIES_DAILY&" +
    "symbol=#{ticker.upcase}&" +
    "apikey=#{SQA.av.key}&" +
    "datatype=csv&" +
    "outputsize=#{full ? 'full' : 'compact'}"
  ).to_hash

  unless 200 == response[:status]
    raise "Bad Response: #{response[:status]}"
  end

  # Read CSV into Polars DataFrame directly
  df = Polars.read_csv(
    StringIO.new(response[:body]),
    dtypes: {
      "open" => :f64,
      "high" => :f64,
      "low" => :f64,
      "close" => :f64,
      "volume" => :i64
    }
  )

  # Handle date criteria if applicable
  if from_date
    # Use Polars.col() to create an expression for filtering
    # Use > (not >=) to exclude the from_date itself and prevent duplicates
    df = df.filter(Polars.col("timestamp") > from_date.to_s)
  end

  # Wrap in SQA::DataFrame with proper transformers
  # Note: mapping is applied first (renames columns), then transformers
  sqa_df = SQA::DataFrame.new(df, transformers: TRANSFORMERS, mapping: HEADER_MAPPING)

  # Alpha Vantage doesn't split close/adjusted_close, so duplicate for compatibility
  # This ensures adj_close_price exists for strategies that expect it
  sqa_df.data = sqa_df.data.with_column(
    sqa_df.data["close_price"].alias("adj_close_price")
  )

  # Sort data in ascending chronological order (oldest to newest) for TA-Lib compatibility
  # Alpha Vantage returns data newest-first, but TA-Lib expects oldest-first
  sqa_df.data = sqa_df.data.sort("timestamp", reverse: false)

  sqa_df
end