yf_as_dataframe 0.2.15

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,304 @@
1
+ require 'polars-df'
2
+
3
+ class YfAsDataframe
4
+ module Financials
5
+ include ActiveSupport::Inflector
6
+
7
+ def self.included(base) # built-in Ruby hook for modules
8
+ base.class_eval do
9
+ original_method = instance_method(:initialize)
10
+ define_method(:initialize) do |*args, &block|
11
+ original_method.bind(self).call(*args, &block)
12
+ initialize_financials # (your module code here)
13
+ end
14
+ end
15
+ end
16
+
17
+ def initialize_financials
18
+ @income_time_series = {}
19
+ @balance_sheet_time_series = {}
20
+ @cash_flow_time_series = {}
21
+ end
22
+
23
+ def income_stmt; _get_income_stmt(pretty: true); end
24
+ def quarterly_income_stmt; _get_income_stmt(pretty: true, freq: 'quarterly'); end
25
+ alias_method :quarterly_incomestmt, :quarterly_income_stmt
26
+ alias_method :quarterly_financials, :quarterly_income_stmt
27
+ alias_method :annual_incomestmt, :income_stmt
28
+ alias_method :annual_income_stmt, :income_stmt
29
+ alias_method :annual_financials, :income_stmt
30
+
31
+ def balance_sheet; _get_balance_sheet(pretty: true); end
32
+ def quarterly_balance_sheet; _get_balance_sheet(pretty: true, freq: 'quarterly'); end
33
+ alias_method :quarterly_balancesheet, :quarterly_balance_sheet
34
+ alias_method :annual_balance_sheet, :balance_sheet
35
+ alias_method :annual_balancesheet, :balance_sheet
36
+
37
+ def cash_flow; _get_cash_flow(pretty: true, freq: 'yearly'); end
38
+ alias_method :cashflow, :cash_flow
39
+ def quarterly_cash_flow; _get_cash_flow(pretty: true, freq: 'quarterly'); end
40
+ alias_method :quarterly_cashflow, :quarterly_cash_flow
41
+ alias_method :annual_cashflow, :cash_flow
42
+ alias_method :annual_cash_flow, :cash_flow
43
+
44
+
45
+
46
+
47
+
48
+
49
+
50
+
51
+
52
+ private
53
+
54
+ def _get_cash_flow(as_dict: false, pretty: false, freq: "yearly")
55
+ data = _get_cash_flow_time_series(freq)
56
+
57
+ if pretty
58
+ # data = data.dup
59
+ # data.index = YfAsDataframe::Utils.camel2title(data.index, sep: ' ', acronyms: ["PPE"])
60
+ end
61
+
62
+ as_dict ? data.to_h : data
63
+ end
64
+
65
+ def _get_income_stmt(as_dict: false, pretty: false, freq: "yearly")
66
+ data = _get_income_time_series(freq)
67
+
68
+ if pretty
69
+ # data = data.dup
70
+ # data.index = YfAsDataframe::Utils.camel2title(data.index, sep: ' ', acronyms: ["EBIT", "EBITDA", "EPS", "NI"])
71
+ end
72
+
73
+ as_dict ? data.to_h : data
74
+ end
75
+
76
+
77
+ def _get_balance_sheet(as_dict: false, pretty: false, freq: "yearly")
78
+ data = _get_balance_sheet_time_series(freq)
79
+
80
+ if pretty
81
+ # data = data.dup
82
+ # data.index = YfAsDataframe::Utils.camel2title(data.index, sep: ' ', acronyms: ["PPE"])
83
+ end
84
+
85
+ as_dict ? data.to_h : data
86
+ end
87
+
88
+ def _get_income_time_series(freq = "yearly")
89
+ res = @income_time_series
90
+ res[freq] ||= _fetch_time_series("income", freq)
91
+ res[freq]
92
+ end
93
+
94
+ def _get_balance_sheet_time_series(freq = "yearly")
95
+ res = @balance_sheet_time_series
96
+ res[freq] ||= _fetch_time_series("balancesheet", freq)
97
+ res[freq]
98
+ end
99
+
100
+ def _get_cash_flow_time_series(freq = "yearly")
101
+ res = @cash_flow_time_series
102
+ res[freq] ||= _fetch_time_series("cashflow", freq)
103
+ res[freq]
104
+ end
105
+
106
+ def _get_financials_time_series(timescale, ts_keys)
107
+ Polars::Config.set_tbl_rows(-1)
108
+ timescale_translation = { "yearly" => "annual", "quarterly" => "quarterly" }
109
+ timescale = timescale_translation[timescale]
110
+
111
+ ts_url_base = "https://query2.finance.yahoo.com/ws/fundamentals-timeseries/v1/finance/timeseries/#{symbol}?symbol=#{symbol}"
112
+ url = ts_url_base + "&type=" + ts_keys.map { |k| "#{timescale}#{k}" }.join(",")
113
+ start_dt = DateTime.new(2016, 12, 31)
114
+ end_dt = DateTime.now.tomorrow.midnight
115
+ url += "&period1=#{start_dt.to_i}&period2=#{end_dt.to_i}"
116
+
117
+ json_str = get(url).parsed_response
118
+ data_raw = json_str["timeseries"]["result"]
119
+ data_raw.each { |d| d.delete("meta") }
120
+
121
+ timestamps = data_raw.map{|d| d['timestamp']}.flatten.uniq.compact.uniq.sort.reverse
122
+
123
+ cols = [ :metric ] + timestamps.map{|ts| Time.at(ts).utc.to_date.to_s }
124
+ df = {}; cols.each {|c| df[c] = [] }
125
+ ts_keys.each { |k| df[:metric] << k.gsub(/([A-Z]+)/,' \1').strip }
126
+
127
+ timestamps.each do |ts|
128
+ ts_date = Time.at(ts).utc.to_date.to_s
129
+
130
+ ts_keys.each_with_index do |k, ndex|
131
+ l = "#{timescale}#{k}"
132
+ d = data_raw.detect{|dd| dd.key?(l) }
133
+ if d.nil?
134
+ df[ts_date] << ''
135
+ next
136
+ end
137
+ tv = d[l].detect{|dd| dd['asOfDate'] == ts_date }
138
+ df[ts_date] << (tv.nil? ? '' : tv['reportedValue']['raw'].to_s)
139
+ end
140
+
141
+ end
142
+
143
+ df = Polars::DataFrame.new(df)
144
+ timestamps.map{|ts| Time.at(ts).utc.to_date.to_s }.each do |t|
145
+ puts t
146
+ df.replace(t, Polars::Series.new(df[t].cast(Polars::String)))
147
+ end
148
+ df
149
+ end
150
+
151
+ def _fetch_time_series(nam, timescale)
152
+ # Rails.logger.info { "#{__FILE__}:#{__LINE__}"}
153
+ allowed_names = FUNDAMENTALS_KEYS.keys + [:income]
154
+ allowed_timescales = ["yearly", "quarterly"]
155
+
156
+ raise ArgumentError, "Illegal argument: name (#{nam}) must be one of: #{allowed_names}" unless allowed_names.include?(nam.to_sym)
157
+ raise ArgumentError, "Illegal argument: timescale (#{timescale}) must be one of: #{allowed_timescales}" unless allowed_timescales.include?(timescale)
158
+
159
+ begin
160
+ statement = _create_financials_table(nam, timescale)
161
+ return statement unless statement.nil?
162
+ rescue Yfin::YfinDataException => e
163
+ Rails.logger.error {"#{@symbol}: Failed to create #{nam} financials table for reason: #{e}"}
164
+ end
165
+ Polars::DataFrame.new()
166
+ end
167
+
168
+ def _create_financials_table(nam, timescale)
169
+ nam = "financials" if nam == "income"
170
+
171
+ keys = FUNDAMENTALS_KEYS[nam.to_sym]
172
+ begin
173
+ _get_financials_time_series(timescale, keys)
174
+ rescue StandardError
175
+ nil
176
+ end
177
+ end
178
+
179
+
180
+
181
+ FUNDAMENTALS_KEYS = {
182
+ financials: [
183
+ "TaxEffectOfUnusualItems", "TaxRateForCalcs", "NormalizedEBITDA", "NormalizedDilutedEPS",
184
+ "NormalizedBasicEPS", "TotalUnusualItems", "TotalUnusualItemsExcludingGoodwill",
185
+ "NetIncomeFromContinuingOperationNetMinorityInterest", "ReconciledDepreciation",
186
+ "ReconciledCostOfRevenue", "EBITDA", "EBIT", "NetInterestIncome", "InterestExpense",
187
+ "InterestIncome", "ContinuingAndDiscontinuedDilutedEPS", "ContinuingAndDiscontinuedBasicEPS",
188
+ "NormalizedIncome", "NetIncomeFromContinuingAndDiscontinuedOperation", "TotalExpenses",
189
+ "RentExpenseSupplemental", "ReportedNormalizedDilutedEPS", "ReportedNormalizedBasicEPS",
190
+ "TotalOperatingIncomeAsReported", "DividendPerShare", "DilutedAverageShares", "BasicAverageShares",
191
+ "DilutedEPS", "DilutedEPSOtherGainsLosses", "TaxLossCarryforwardDilutedEPS",
192
+ "DilutedAccountingChange", "DilutedExtraordinary", "DilutedDiscontinuousOperations",
193
+ "DilutedContinuousOperations", "BasicEPS", "BasicEPSOtherGainsLosses", "TaxLossCarryforwardBasicEPS",
194
+ "BasicAccountingChange", "BasicExtraordinary", "BasicDiscontinuousOperations",
195
+ "BasicContinuousOperations", "DilutedNIAvailtoComStockholders", "AverageDilutionEarnings",
196
+ "NetIncomeCommonStockholders", "OtherunderPreferredStockDividend", "PreferredStockDividends",
197
+ "NetIncome", "MinorityInterests", "NetIncomeIncludingNoncontrollingInterests",
198
+ "NetIncomeFromTaxLossCarryforward", "NetIncomeExtraordinary", "NetIncomeDiscontinuousOperations",
199
+ "NetIncomeContinuousOperations", "EarningsFromEquityInterestNetOfTax", "TaxProvision",
200
+ "PretaxIncome", "OtherIncomeExpense", "OtherNonOperatingIncomeExpenses", "SpecialIncomeCharges",
201
+ "GainOnSaleOfPPE", "GainOnSaleOfBusiness", "OtherSpecialCharges", "WriteOff",
202
+ "ImpairmentOfCapitalAssets", "RestructuringAndMergernAcquisition", "SecuritiesAmortization",
203
+ "EarningsFromEquityInterest", "GainOnSaleOfSecurity", "NetNonOperatingInterestIncomeExpense",
204
+ "TotalOtherFinanceCost", "InterestExpenseNonOperating", "InterestIncomeNonOperating",
205
+ "OperatingIncome", "OperatingExpense", "OtherOperatingExpenses", "OtherTaxes",
206
+ "ProvisionForDoubtfulAccounts", "DepreciationAmortizationDepletionIncomeStatement",
207
+ "DepletionIncomeStatement", "DepreciationAndAmortizationInIncomeStatement", "Amortization",
208
+ "AmortizationOfIntangiblesIncomeStatement", "DepreciationIncomeStatement", "ResearchAndDevelopment",
209
+ "SellingGeneralAndAdministration", "SellingAndMarketingExpense", "GeneralAndAdministrativeExpense",
210
+ "OtherGandA", "InsuranceAndClaims", "RentAndLandingFees", "SalariesAndWages", "GrossProfit",
211
+ "CostOfRevenue", "TotalRevenue", "ExciseTaxes", "OperatingRevenue"
212
+ ],
213
+ balancesheet: [
214
+ "TreasurySharesNumber", "PreferredSharesNumber", "OrdinarySharesNumber", "ShareIssued", "NetDebt",
215
+ "TotalDebt", "TangibleBookValue", "InvestedCapital", "WorkingCapital", "NetTangibleAssets",
216
+ "CapitalLeaseObligations", "CommonStockEquity", "PreferredStockEquity", "TotalCapitalization",
217
+ "TotalEquityGrossMinorityInterest", "MinorityInterest", "StockholdersEquity",
218
+ "OtherEquityInterest", "GainsLossesNotAffectingRetainedEarnings", "OtherEquityAdjustments",
219
+ "FixedAssetsRevaluationReserve", "ForeignCurrencyTranslationAdjustments",
220
+ "MinimumPensionLiabilities", "UnrealizedGainLoss", "TreasuryStock", "RetainedEarnings",
221
+ "AdditionalPaidInCapital", "CapitalStock", "OtherCapitalStock", "CommonStock", "PreferredStock",
222
+ "TotalPartnershipCapital", "GeneralPartnershipCapital", "LimitedPartnershipCapital",
223
+ "TotalLiabilitiesNetMinorityInterest", "TotalNonCurrentLiabilitiesNetMinorityInterest",
224
+ "OtherNonCurrentLiabilities", "LiabilitiesHeldforSaleNonCurrent", "RestrictedCommonStock",
225
+ "PreferredSecuritiesOutsideStockEquity", "DerivativeProductLiabilities", "EmployeeBenefits",
226
+ "NonCurrentPensionAndOtherPostretirementBenefitPlans", "NonCurrentAccruedExpenses",
227
+ "DuetoRelatedPartiesNonCurrent", "TradeandOtherPayablesNonCurrent",
228
+ "NonCurrentDeferredLiabilities", "NonCurrentDeferredRevenue",
229
+ "NonCurrentDeferredTaxesLiabilities", "LongTermDebtAndCapitalLeaseObligation",
230
+ "LongTermCapitalLeaseObligation", "LongTermDebt", "LongTermProvisions", "CurrentLiabilities",
231
+ "OtherCurrentLiabilities", "CurrentDeferredLiabilities", "CurrentDeferredRevenue",
232
+ "CurrentDeferredTaxesLiabilities", "CurrentDebtAndCapitalLeaseObligation",
233
+ "CurrentCapitalLeaseObligation", "CurrentDebt", "OtherCurrentBorrowings", "LineOfCredit",
234
+ "CommercialPaper", "CurrentNotesPayable", "PensionandOtherPostRetirementBenefitPlansCurrent",
235
+ "CurrentProvisions", "PayablesAndAccruedExpenses", "CurrentAccruedExpenses", "InterestPayable",
236
+ "Payables", "OtherPayable", "DuetoRelatedPartiesCurrent", "DividendsPayable", "TotalTaxPayable",
237
+ "IncomeTaxPayable", "AccountsPayable", "TotalAssets", "TotalNonCurrentAssets",
238
+ "OtherNonCurrentAssets", "DefinedPensionBenefit", "NonCurrentPrepaidAssets",
239
+ "NonCurrentDeferredAssets", "NonCurrentDeferredTaxesAssets", "DuefromRelatedPartiesNonCurrent",
240
+ "NonCurrentNoteReceivables", "NonCurrentAccountsReceivable", "FinancialAssets",
241
+ "InvestmentsAndAdvances", "OtherInvestments", "InvestmentinFinancialAssets",
242
+ "HeldToMaturitySecurities", "AvailableForSaleSecurities",
243
+ "FinancialAssetsDesignatedasFairValueThroughProfitorLossTotal", "TradingSecurities",
244
+ "LongTermEquityInvestment", "InvestmentsinJointVenturesatCost",
245
+ "InvestmentsInOtherVenturesUnderEquityMethod", "InvestmentsinAssociatesatCost",
246
+ "InvestmentsinSubsidiariesatCost", "InvestmentProperties", "GoodwillAndOtherIntangibleAssets",
247
+ "OtherIntangibleAssets", "Goodwill", "NetPPE", "AccumulatedDepreciation", "GrossPPE", "Leases",
248
+ "ConstructionInProgress", "OtherProperties", "MachineryFurnitureEquipment",
249
+ "BuildingsAndImprovements", "LandAndImprovements", "Properties", "CurrentAssets",
250
+ "OtherCurrentAssets", "HedgingAssetsCurrent", "AssetsHeldForSaleCurrent", "CurrentDeferredAssets",
251
+ "CurrentDeferredTaxesAssets", "RestrictedCash", "PrepaidAssets", "Inventory",
252
+ "InventoriesAdjustmentsAllowances", "OtherInventories", "FinishedGoods", "WorkInProcess",
253
+ "RawMaterials", "Receivables", "ReceivablesAdjustmentsAllowances", "OtherReceivables",
254
+ "DuefromRelatedPartiesCurrent", "TaxesReceivable", "AccruedInterestReceivable", "NotesReceivable",
255
+ "LoansReceivable", "AccountsReceivable", "AllowanceForDoubtfulAccountsReceivable",
256
+ "GrossAccountsReceivable", "CashCashEquivalentsAndShortTermInvestments",
257
+ "OtherShortTermInvestments", "CashAndCashEquivalents", "CashEquivalents", "CashFinancial"
258
+ ],
259
+ cashflow: [
260
+ "ForeignSales", "DomesticSales", "AdjustedGeographySegmentData", "FreeCashFlow",
261
+ "RepurchaseOfCapitalStock", "RepaymentOfDebt", "IssuanceOfDebt", "IssuanceOfCapitalStock",
262
+ "CapitalExpenditure", "InterestPaidSupplementalData", "IncomeTaxPaidSupplementalData",
263
+ "EndCashPosition", "OtherCashAdjustmentOutsideChangeinCash", "BeginningCashPosition",
264
+ "EffectOfExchangeRateChanges", "ChangesInCash", "OtherCashAdjustmentInsideChangeinCash",
265
+ "CashFlowFromDiscontinuedOperation", "FinancingCashFlow", "CashFromDiscontinuedFinancingActivities",
266
+ "CashFlowFromContinuingFinancingActivities", "NetOtherFinancingCharges", "InterestPaidCFF",
267
+ "ProceedsFromStockOptionExercised", "CashDividendsPaid", "PreferredStockDividendPaid",
268
+ "CommonStockDividendPaid", "NetPreferredStockIssuance", "PreferredStockPayments",
269
+ "PreferredStockIssuance", "NetCommonStockIssuance", "CommonStockPayments", "CommonStockIssuance",
270
+ "NetIssuancePaymentsOfDebt", "NetShortTermDebtIssuance", "ShortTermDebtPayments",
271
+ "ShortTermDebtIssuance", "NetLongTermDebtIssuance", "LongTermDebtPayments", "LongTermDebtIssuance",
272
+ "InvestingCashFlow", "CashFromDiscontinuedInvestingActivities",
273
+ "CashFlowFromContinuingInvestingActivities", "NetOtherInvestingChanges", "InterestReceivedCFI",
274
+ "DividendsReceivedCFI", "NetInvestmentPurchaseAndSale", "SaleOfInvestment", "PurchaseOfInvestment",
275
+ "NetInvestmentPropertiesPurchaseAndSale", "SaleOfInvestmentProperties",
276
+ "PurchaseOfInvestmentProperties", "NetBusinessPurchaseAndSale", "SaleOfBusiness",
277
+ "PurchaseOfBusiness", "NetIntangiblesPurchaseAndSale", "SaleOfIntangibles", "PurchaseOfIntangibles",
278
+ "NetPPEPurchaseAndSale", "SaleOfPPE", "PurchaseOfPPE", "CapitalExpenditureReported",
279
+ "OperatingCashFlow", "CashFromDiscontinuedOperatingActivities",
280
+ "CashFlowFromContinuingOperatingActivities", "TaxesRefundPaid", "InterestReceivedCFO",
281
+ "InterestPaidCFO", "DividendReceivedCFO", "DividendPaidCFO", "ChangeInWorkingCapital",
282
+ "ChangeInOtherWorkingCapital", "ChangeInOtherCurrentLiabilities", "ChangeInOtherCurrentAssets",
283
+ "ChangeInPayablesAndAccruedExpense", "ChangeInAccruedExpense", "ChangeInInterestPayable",
284
+ "ChangeInPayable", "ChangeInDividendPayable", "ChangeInAccountPayable", "ChangeInTaxPayable",
285
+ "ChangeInIncomeTaxPayable", "ChangeInPrepaidAssets", "ChangeInInventory", "ChangeInReceivables",
286
+ "ChangesInAccountReceivables", "OtherNonCashItems", "ExcessTaxBenefitFromStockBasedCompensation",
287
+ "StockBasedCompensation", "UnrealizedGainLossOnInvestmentSecurities", "ProvisionandWriteOffofAssets",
288
+ "AssetImpairmentCharge", "AmortizationOfSecurities", "DeferredTax", "DeferredIncomeTax",
289
+ "DepreciationAmortizationDepletion", "Depletion", "DepreciationAndAmortization",
290
+ "AmortizationCashFlow", "AmortizationOfIntangibles", "Depreciation", "OperatingGainsLosses",
291
+ "PensionAndEmployeeBenefitExpense", "EarningsLossesFromEquityInvestments",
292
+ "GainLossOnInvestmentSecurities", "NetForeignCurrencyExchangeGainLoss", "GainLossOnSaleOfPPE",
293
+ "GainLossOnSaleOfBusiness", "NetIncomeFromContinuingOperations",
294
+ "CashFlowsfromusedinOperatingActivitiesDirect", "TaxesRefundPaidDirect", "InterestReceivedDirect",
295
+ "InterestPaidDirect", "DividendsReceivedDirect", "DividendsPaidDirect", "ClassesofCashPayments",
296
+ "OtherCashPaymentsfromOperatingActivities", "PaymentsonBehalfofEmployees",
297
+ "PaymentstoSuppliersforGoodsandServices", "ClassesofCashReceiptsfromOperatingActivities",
298
+ "OtherCashReceiptsfromOperatingActivities", "ReceiptsfromGovernmentGrants", "ReceiptsfromCustomers"
299
+ ]
300
+ }
301
+
302
+
303
+ end
304
+ end
@@ -0,0 +1,53 @@
1
+ class YfAsDataframe
2
+ module Fundamentals
3
+ extend ActiveSupport::Concern
4
+
5
+ def self.included(base) # built-in Ruby hook for modules
6
+ base.class_eval do
7
+ attr_reader :financials, :earnings, :shares, :ticker
8
+
9
+ original_method = instance_method(:initialize)
10
+ define_method(:initialize) do |*args, &block|
11
+ original_method.bind(self).call(*args, &block)
12
+ initialize_fundamentals # (your module code here)
13
+ end
14
+ end
15
+ end
16
+
17
+ def initialize_fundamentals
18
+ @earnings = nil
19
+ @financials = nil
20
+ @shares = nil
21
+
22
+ @financials_data = nil
23
+ @fin_data_quote = nil
24
+ @basics_already_scraped = false
25
+ end
26
+
27
+ # delegate :proxy, :tz, to: :ticker
28
+
29
+ def earnings
30
+ raise YFNotImplementedError.new('earnings') if @earnings.nil?
31
+ @earnings
32
+ end
33
+
34
+ def shares
35
+ raise YFNotImplementedError.new('shares') if @shares.nil?
36
+ @shares
37
+ end
38
+
39
+ # financials_methods = [:income_stmt, :incomestmt, :financials, :balance_sheet, :balancesheet, :cash_flow, :cashflow]
40
+ # financials_methods.each do |meth|
41
+ # delegate "get_#{meth}".to_sym, meth, to: :financials
42
+ # end
43
+
44
+ # fundamentals_methods = [:earnings, :shares]
45
+ # fundamentals_methods.each do |meth|
46
+ # alias_method "get_#{meth}".to_sym, meth
47
+ # end
48
+
49
+ # def quarterly_earnings
50
+ # earnings(freq: 'quarterly')
51
+ # end
52
+ end
53
+ end
@@ -0,0 +1,253 @@
1
+ class YfAsDataframe
2
+ module Holders
3
+ extend ActiveSupport::Concern
4
+ # include YfConnection
5
+
6
+ BASE_URL = 'https://query2.finance.yahoo.com'.freeze
7
+ QUOTE_SUMMARY_URL = "#{BASE_URL}/v10/finance/quoteSummary/".freeze
8
+
9
+ # attr_accessor :ticker
10
+
11
+ def self.included(base) # built-in Ruby hook for modules
12
+ base.class_eval do
13
+ original_method = instance_method(:initialize)
14
+ define_method(:initialize) do |*args, &block|
15
+ original_method.bind(self).call(*args, &block)
16
+ initialize_holders # (your module code here)
17
+ end
18
+ end
19
+ end
20
+
21
+ def initialize_holders
22
+ @major = nil
23
+ @major_direct_holders = nil
24
+ @institutional = nil
25
+ @mutualfund = nil
26
+
27
+ @insider_transactions = nil
28
+ @insider_purchases = nil
29
+ @insider_roster = nil
30
+ end
31
+
32
+ def major
33
+ _fetch_and_parse if @major.nil?
34
+ return @major
35
+ end
36
+
37
+ alias_method :major_holders, :major
38
+
39
+ def institutional
40
+ _fetch_and_parse if @institutional.nil?
41
+ return @institutional
42
+ end
43
+
44
+ alias_method :institutional_holders, :institutional
45
+
46
+ def mutualfund
47
+ _fetch_and_parse if @mutualfund.nil?
48
+ return @mutualfund
49
+ end
50
+
51
+ alias_method :mutualfund_holders, :mutualfund
52
+
53
+ def insider_transactions
54
+ _fetch_and_parse if @insider_transactions.nil?
55
+ return @insider_transactions
56
+ end
57
+
58
+ def insider_purchases
59
+ _fetch_and_parse if @insider_purchases.nil?
60
+ return @insider_purchases
61
+ end
62
+
63
+ def insider_roster
64
+ return @insider_roster unless @insider_roster.nil?
65
+
66
+ _fetch_and_parse
67
+ return @insider_roster
68
+ end
69
+
70
+ alias_method :insider_roster_holders, :insider_roster
71
+
72
+ # holders_methods = [:major, :major_holders, :institutional, :institutional_holders, :mutualfund, \
73
+ # :mutualfund_holders, :insider_transactions, :insider_purchases, :insider_roster, \
74
+ # :insider_roster_holders]
75
+ # holders_methods.each do |meth|
76
+ # alias_method "get_#{meth}".to_sym, meth
77
+ # end
78
+
79
+
80
+
81
+
82
+
83
+ private
84
+
85
+ def _fetch_for_parse(params) #(self, proxy, modules: list)
86
+ # raise YahooFinanceException("Should provide a list of modules, see available modules using `valid_modules`") if !modules.is_a?(Array)
87
+
88
+ # modules = modules.intersection(QUOTE_SUMMARY_VALID_MODULES) #[m for m in modules if m in quote_summary_valid_modules])
89
+
90
+ modules = params[:modules]
91
+
92
+ raise YahooFinanceException("No valid modules provided.") if modules.empty?
93
+
94
+ params_dict = {"modules": modules, "corsDomain": "finance.yahoo.com", "formatted": "false", "symbol": symbol}
95
+
96
+ begin
97
+ result = get_raw_json(QUOTE_SUMMARY_URL + "/#{symbol}", user_agent_headers=user_agent_headers, params=params_dict)
98
+ # Rails.logger.info { "#{__FILE__}:#{__LINE__} result = #{result.inspect}" }
99
+ rescue Exception => e
100
+ Rails.logger.error("ERROR: #{e.message}")
101
+ return nil
102
+ end
103
+ return result
104
+ end
105
+
106
+ # def _fetch_for_parse(params)
107
+ # url = "#{QUOTE_SUMMARY_URL}#{symbol}"
108
+ # Rails.logger.info { "#{__FILE__}:#{__LINE__} url: #{url}, params = #{params.inspect}" }
109
+ # get(url).parsed_response
110
+
111
+ # # JSON.parse(URI.open(url, proxy: proxy, 'User-Agent' => 'Mozilla/5.0 (compatible; yahoo-finance2/0.0.1)').read(query: params))
112
+ # end
113
+
114
+ def _fetch_and_parse
115
+ modules = ['institutionOwnership', 'fundOwnership', 'majorDirectHolders', 'majorHoldersBreakdown',
116
+ 'insiderTransactions', 'insiderHolders', 'netSharePurchaseActivity'].join(',')
117
+ # Rails.logger.info { "#{__FILE__}:#{__LINE__} modules = #{modules.inspect}"}
118
+ params = { modules: modules, corsDomain: 'finance.yahoo.com', formatted: 'false' }
119
+ result = _fetch_for_parse(params)
120
+
121
+ _parse_result(result)
122
+ rescue OpenURI::HTTPError => e
123
+ # Rails.logger.error { "#{__FILE__}:#{__LINE__} Error: #{e.message}" }
124
+
125
+ @major = []
126
+ @major_direct_holders = []
127
+ @institutional = []
128
+ @mutualfund = []
129
+ @insider_transactions = []
130
+ @insider_purchases = []
131
+ @insider_roster = []
132
+ end
133
+
134
+ def _parse_result(result)
135
+ data = result.parsed_response['quoteSummary']['result'].first #.dig('quoteSummary', 'result', 0)
136
+ Rails.logger.info { "#{__FILE__}:#{__LINE__} data = #{data.inspect}" }
137
+ _parse_institution_ownership(data['institutionOwnership'])
138
+ _parse_fund_ownership(data['fundOwnership'])
139
+ _parse_major_holders_breakdown(data['majorHoldersBreakdown'])
140
+ _parse_insider_transactions(data['insiderTransactions'])
141
+ _parse_insider_holders(data['insiderHolders'])
142
+ _parse_net_share_purchase_activity(data['netSharePurchaseActivity'])
143
+ rescue NoMethodError
144
+ raise "Failed to parse holders json data."
145
+ end
146
+
147
+ def _parse_raw_values(data)
148
+ data.is_a?(Hash) && data.key?('raw') ? data['raw'] : data
149
+ end
150
+
151
+ def _parse_institution_ownership(data)
152
+ holders = data['ownershipList'].map { |owner| owner.transform_values { |v| _parse_raw_values(v) }.except('maxAge') }
153
+
154
+ @institutional = holders.map do |holder|
155
+ {
156
+ 'Date Reported' => DateTime.strptime(holder['reportDate'].to_s, '%s'),
157
+ 'Holder' => holder['organization'],
158
+ 'Shares' => holder['position'],
159
+ 'Value' => holder['value']
160
+ }
161
+ end
162
+ end
163
+
164
+ def _parse_fund_ownership(data)
165
+ holders = data['ownershipList'].map { |owner| owner.transform_values { |v| _parse_raw_values(v) }.except('maxAge') }
166
+
167
+ @mutualfund = holders.map do |holder|
168
+ {
169
+ 'Date Reported' => DateTime.strptime(holder['reportDate'].to_s, '%s'),
170
+ 'Holder' => holder['organization'],
171
+ 'Shares' => holder['position'],
172
+ 'Value' => holder['value']
173
+ }
174
+ end
175
+ end
176
+
177
+ def _parse_major_holders_breakdown(data)
178
+ data.except!('maxAge') if data.key?('maxAge')
179
+ @major = data.map { |k, v| [k, _parse_raw_values(v)] }.to_h
180
+ end
181
+
182
+ def _parse_insider_transactions(data)
183
+ holders = data['transactions'].map { |owner| owner.transform_values { |v| _parse_raw_values(v) }.except('maxAge') }
184
+
185
+ @insider_transactions = holders.map do |holder|
186
+ {
187
+ 'Start Date' => DateTime.strptime(holder['startDate'].to_s, '%s'),
188
+ 'Insider' => holder['filerName'],
189
+ 'Position' => holder['filerRelation'],
190
+ 'URL' => holder['filerUrl'],
191
+ 'Transaction' => holder['moneyText'],
192
+ 'Text' => holder['transactionText'],
193
+ 'Shares' => holder['shares'],
194
+ 'Value' => holder['value'],
195
+ 'Ownership' => holder['ownership']
196
+ }
197
+ end
198
+ end
199
+
200
+ def _parse_insider_holders(data)
201
+ holders = data['holders'].map { |owner| owner.transform_values { |v| _parse_raw_values(v) }.except('maxAge') }
202
+
203
+ @insider_roster = holders.map do |holder|
204
+ {
205
+ 'Name' => holder['name'].to_s,
206
+ 'Position' => holder['relation'].to_s,
207
+ 'URL' => holder['url'].to_s,
208
+ 'Most Recent Transaction' => holder['transactionDescription'].to_s,
209
+ 'Latest Transaction Date' => holder['latestTransDate'] ? DateTime.strptime(holder['latestTransDate'].to_s, '%s') : nil,
210
+ 'Position Direct Date' => DateTime.strptime(holder['positionDirectDate'].to_s, '%s'),
211
+ 'Shares Owned Directly' => holder['positionDirect'],
212
+ 'Position Indirect Date' => holder['positionIndirectDate'] ? DateTime.strptime(holder['positionIndirectDate'].to_s, '%s') : nil,
213
+ 'Shares Owned Indirectly' => holder['positionIndirect']
214
+ }
215
+ end
216
+ end
217
+
218
+ def _parse_net_share_purchase_activity(data)
219
+ period = data['period'] || ''
220
+ @insider_purchases = {
221
+ "Insider Purchases Last #{period}" => [
222
+ 'Purchases',
223
+ 'Sales',
224
+ 'Net Shares Purchased (Sold)',
225
+ 'Total Insider Shares Held',
226
+ '% Net Shares Purchased (Sold)',
227
+ '% Buy Shares',
228
+ '% Sell Shares'
229
+ ],
230
+ 'Shares' => [
231
+ data['buyInfoShares'],
232
+ data['sellInfoShares'],
233
+ data['netInfoShares'],
234
+ data['totalInsiderShares'],
235
+ data['netPercentInsiderShares'],
236
+ data['buyPercentInsiderShares'],
237
+ data['sellPercentInsiderShares']
238
+ ],
239
+ 'Trans' => [
240
+ data['buyInfoCount'],
241
+ data['sellInfoCount'],
242
+ data['netInfoCount'],
243
+ nil,
244
+ nil,
245
+ nil,
246
+ nil
247
+ ]
248
+ }
249
+ end
250
+ end
251
+
252
+
253
+ end