ruby-edgar 0.1.0

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,521 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Edgar
4
+ # Form-type dispatching: maps filing forms to specialized data objects.
5
+ module DataObjects
6
+ # Base class for form-specific data wrappers.
7
+ class Base
8
+ attr_reader :filing
9
+
10
+ def initialize(filing)
11
+ @filing = filing
12
+ end
13
+
14
+ def form_type
15
+ @filing.form
16
+ end
17
+
18
+ def company_name
19
+ @filing.company
20
+ end
21
+
22
+ def filing_date
23
+ @filing.filing_date
24
+ end
25
+
26
+ def accession_number
27
+ @filing.accession_number
28
+ end
29
+
30
+ def to_s
31
+ "#<#{self.class.name.split('::').last} #{form_type} #{company_name}>"
32
+ end
33
+ end
34
+
35
+ # Wraps a 10-K annual filing with section accessors.
36
+ class TenK < Base
37
+ def items
38
+ @filing.sections
39
+ end
40
+
41
+ def business
42
+ items["1"]
43
+ end
44
+
45
+ def risk_factors
46
+ items["1A"]
47
+ end
48
+
49
+ def financial_statements
50
+ items["8"]
51
+ end
52
+
53
+ def exhibits
54
+ @filing.exhibits
55
+ end
56
+ end
57
+
58
+ # Wraps a 10-Q quarterly filing with section accessors.
59
+ class TenQ < Base
60
+ def items
61
+ @filing.sections
62
+ end
63
+
64
+ def financial_statements
65
+ items["1"]
66
+ end
67
+
68
+ def legal_proceedings
69
+ items["1"]
70
+ end
71
+ end
72
+
73
+ # Wraps a Form 4 insider trading filing.
74
+ class Form4 < Base
75
+ def transactions
76
+ @filing.xbrl&.financial_concepts || []
77
+ end
78
+
79
+ def issuer_cik
80
+ @filing.cik
81
+ end
82
+ end
83
+
84
+ # Wraps an 8-K current report filing.
85
+ class Form8K < Base
86
+ def items
87
+ @filing.sections
88
+ end
89
+
90
+ def exhibit_list
91
+ @filing.exhibits
92
+ end
93
+ end
94
+
95
+ # Wraps a Schedule 13D beneficial ownership filing.
96
+ class Schedule13D < Base
97
+ def beneficial_owner
98
+ @filing.header.filers.first
99
+ end
100
+ end
101
+
102
+ # Wraps a Schedule 13G beneficial ownership filing.
103
+ class Schedule13G < Base
104
+ def beneficial_owner
105
+ @filing.header.filers.first
106
+ end
107
+ end
108
+
109
+ # Wraps an NPORT-P portfolio holding filing.
110
+ class FormNport < Base
111
+ def report_date
112
+ @filing.header.period_of_report
113
+ end
114
+
115
+ def fund_name
116
+ @filing.header.filer_company
117
+ end
118
+ end
119
+
120
+ # Wraps a Form 13F institutional holdings filing.
121
+ class Form13F < Base
122
+ def report_date
123
+ @filing.header.period_of_report
124
+ end
125
+
126
+ def filing_manager
127
+ @filing.header.filer_company
128
+ end
129
+
130
+ def holdings
131
+ xml = @filing.xml
132
+ return [] if xml.empty?
133
+
134
+ doc = Nokogiri::XML(xml)
135
+ doc.remove_namespaces!
136
+ entries = []
137
+ doc.xpath("//infoTable").each do |table|
138
+ entry = {
139
+ name_of_issuer: table.at_xpath("nameOfIssuer")&.text,
140
+ title_of_class: table.at_xpath("titleOfClass")&.text,
141
+ cusip: table.at_xpath("cusip")&.text,
142
+ value: table.at_xpath("value")&.text&.to_i,
143
+ shares: table.at_xpath("shrsOrPrnAmt/shrsPrnAmt")&.text&.to_i,
144
+ put_call: table.at_xpath("putCall")&.text,
145
+ investment_discretion: table.at_xpath("investmentDiscretion")&.text,
146
+ voting_authority_sole: table.at_xpath("votingAuthority/sole")&.text&.to_i,
147
+ voting_authority_shared: table.at_xpath("votingAuthority/shared")&.text&.to_i,
148
+ voting_authority_none: table.at_xpath("votingAuthority/none")&.text&.to_i
149
+ }
150
+ entries << entry
151
+ end
152
+ entries
153
+ rescue StandardError
154
+ []
155
+ end
156
+ end
157
+
158
+ # Wraps a filing summary report.
159
+ class FilingSummary < Base
160
+ end
161
+
162
+ # Wraps a Form 3 initial insider ownership filing.
163
+ class Form3 < Base
164
+ def issuer_cik
165
+ @filing.cik
166
+ end
167
+ end
168
+
169
+ # Wraps a Form 5 annual insider ownership filing.
170
+ class Form5 < Base
171
+ def issuer_cik
172
+ @filing.cik
173
+ end
174
+ end
175
+
176
+ # Wraps a proxy statement (DEF 14A, etc.).
177
+ class ProxyStatement < Base
178
+ def items
179
+ @filing.sections
180
+ end
181
+
182
+ def proposals
183
+ items
184
+ end
185
+ end
186
+
187
+ # Wraps an S-1 IPO registration filing.
188
+ class S1 < Base
189
+ def items
190
+ @filing.sections
191
+ end
192
+
193
+ def business
194
+ items["1"]
195
+ end
196
+
197
+ def risk_factors
198
+ items["1A"]
199
+ end
200
+
201
+ def financial_statements
202
+ items["8"]
203
+ end
204
+ end
205
+
206
+ # Wraps a Form 20-F foreign filer annual report.
207
+ class Form20F < Base
208
+ def items
209
+ @filing.sections
210
+ end
211
+ end
212
+
213
+ # Wraps a Form 6-K foreign filer current report.
214
+ class Form6K < Base
215
+ def items
216
+ @filing.sections
217
+ end
218
+ end
219
+
220
+ # Wraps an N-CSR fund annual report.
221
+ class FormNCSR < Base
222
+ end
223
+
224
+ # Wraps a Form 144 restricted stock filing.
225
+ class Form144 < Base
226
+ def securities_sold
227
+ @filing.header.fields["SECURITIES SOLD"] || []
228
+ end
229
+
230
+ def issuer
231
+ @filing.header.filer_company
232
+ end
233
+ end
234
+
235
+ # Wraps a Form C crowdfunding filing.
236
+ class FormC < Base
237
+ def offering_amount
238
+ @filing.header.fields["TOTAL OFFERING AMOUNT"]&.first
239
+ end
240
+
241
+ def issuer
242
+ @filing.header.filer_company
243
+ end
244
+ end
245
+
246
+ # Wraps a Form D offering filing.
247
+ class FormD < Base
248
+ def offering_amount
249
+ @filing.header.fields["TOTAL OFFERING AMOUNT"]&.first
250
+ end
251
+
252
+ def issuer
253
+ @filing.header.filer_company
254
+ end
255
+ end
256
+
257
+ # Wraps an S-3 shelf registration filing.
258
+ class RegistrationS3 < Base
259
+ def items
260
+ @filing.sections
261
+ end
262
+
263
+ def risk_factors
264
+ items["1A"]
265
+ end
266
+ end
267
+
268
+ # Wraps a 424B prospectus filing.
269
+ class Prospectus424B < Base
270
+ def items
271
+ @filing.sections
272
+ end
273
+ end
274
+
275
+ # Wraps a SEC correspondence filing.
276
+ class Correspondence < Base
277
+ def subject
278
+ @filing.header.fields["SUBJECT"]&.first
279
+ end
280
+ end
281
+
282
+ # Wraps an SEC Effect filing.
283
+ class Effect < Base
284
+ def effect_type
285
+ @filing.header.fields["EFFECT"]&.first
286
+ end
287
+ end
288
+
289
+ # Wraps an Alternative Trading System filing.
290
+ class AlternativeTradingSystem < Base
291
+ def ats_name
292
+ @filing.header.filer_company
293
+ end
294
+ end
295
+
296
+ # Wraps a Draft Registration Statement filing.
297
+ class DraftRegistrationStatement < Base
298
+ def items
299
+ @filing.sections
300
+ end
301
+ end
302
+
303
+ # Wraps a 40-F Canadian MJDS annual report.
304
+ class FortyF < Form20F
305
+ end
306
+
307
+ # Wraps an F-1 foreign registration statement.
308
+ class RegistrationS1 < S1
309
+ end
310
+
311
+ # Wraps a Fund portfolio holdings report (NPORT-P).
312
+ class FundReport < FormNport
313
+ end
314
+
315
+ # Wraps a Fund shareholder report (N-CSR / N-CSRS).
316
+ class FundShareholderReport < FormNCSR
317
+ end
318
+
319
+ # Wraps a Money Market Fund report (N-MFP2 / N-MFP3).
320
+ class MoneyMarketFund < Base
321
+ def report_date
322
+ @filing.header.period_of_report
323
+ end
324
+
325
+ def fund_name
326
+ @filing.header.filer_company
327
+ end
328
+ end
329
+
330
+ # Wraps a Fund census report (N-CEN).
331
+ class FundCensus < Base
332
+ def report_date
333
+ @filing.header.period_of_report
334
+ end
335
+
336
+ def fund_name
337
+ @filing.header.filer_company
338
+ end
339
+ end
340
+
341
+ # Wraps an N-PX proxy voting record.
342
+ class NPX < Base
343
+ def report_date
344
+ @filing.header.period_of_report
345
+ end
346
+
347
+ def fund_name
348
+ @filing.header.filer_company
349
+ end
350
+ end
351
+
352
+ # Wraps a 497K summary prospectus.
353
+ class Prospectus497K < Base
354
+ def fund_name
355
+ @filing.header.filer_company
356
+ end
357
+ end
358
+
359
+ # Wraps a 24F-2NT fund fee notice.
360
+ class FundFeeNotice < Base
361
+ def fund_name
362
+ @filing.header.filer_company
363
+ end
364
+ end
365
+
366
+ # Wraps a Municipal Advisor Form (MA-I).
367
+ class MunicipalAdvisorForm < Base
368
+ def firm_name
369
+ @filing.header.filer_company
370
+ end
371
+ end
372
+
373
+ # Wraps an ABS distribution report (10-D).
374
+ class TenD < Base
375
+ def items
376
+ @filing.sections
377
+ end
378
+
379
+ def distribution_data
380
+ @filing.attachments.select { |a| a.document_type == "EX-102" }
381
+ end
382
+ end
383
+
384
+ # Generic wrapper for XML-based SEC filings (X-17A-5, TA-1, TA-2, etc.).
385
+ class XmlFiling < Base
386
+ def xml_data
387
+ @filing.xml
388
+ end
389
+
390
+ def parsed_xml
391
+ return nil if xml_data.empty?
392
+
393
+ Nokogiri::XML(xml_data)
394
+ rescue StandardError
395
+ nil
396
+ end
397
+ end
398
+
399
+ # Wraps an Alternative Trading System Cessation filing (ATS-N-C).
400
+ class AtsCessation < Base
401
+ def ats_name
402
+ @filing.header.filer_company
403
+ end
404
+ end
405
+
406
+ # Python-compatible alias classes
407
+ EightK = Form8K
408
+ SixK = Form6K
409
+ TwentyF = Form20F
410
+
411
+ FORM_MAP = {
412
+ "10-K" => TenK,
413
+ "10-K/A" => TenK,
414
+ "10-Q" => TenQ,
415
+ "10-Q/A" => TenQ,
416
+ "4" => Form4,
417
+ "4/A" => Form4,
418
+ "8-K" => EightK,
419
+ "8-K/A" => EightK,
420
+ "6-K" => SixK,
421
+ "6-K/A" => SixK,
422
+ "20-F" => TwentyF,
423
+ "20-F/A" => TwentyF,
424
+ "40-F" => FortyF,
425
+ "40-F/A" => FortyF,
426
+ "SC 13D" => Schedule13D,
427
+ "SC 13D/A" => Schedule13D,
428
+ "SC 13G" => Schedule13G,
429
+ "SC 13G/A" => Schedule13G,
430
+ "NPORT-P" => FundReport,
431
+ "NPORT-P/A" => FundReport,
432
+ "NPORT-EX" => FundReport,
433
+ "13F-HR" => Form13F,
434
+ "13F-HR/A" => Form13F,
435
+ "3" => Form3,
436
+ "3/A" => Form3,
437
+ "5" => Form5,
438
+ "5/A" => Form5,
439
+ "144" => Form144,
440
+ "144/A" => Form144,
441
+ "DEF 14A" => ProxyStatement,
442
+ "DEFA 14A" => ProxyStatement,
443
+ "DEFM 14A" => ProxyStatement,
444
+ "DEFC 14A" => ProxyStatement,
445
+ "DEFN 14A" => ProxyStatement,
446
+ "DFAN 14A" => ProxyStatement,
447
+ "DEFR 14A" => ProxyStatement,
448
+ "DFRN 14A" => ProxyStatement,
449
+ "PRE 14A" => ProxyStatement,
450
+ "PREC 14A" => ProxyStatement,
451
+ "PREN 14A" => ProxyStatement,
452
+ "PREM 14A" => ProxyStatement,
453
+ "S-1" => RegistrationS1,
454
+ "S-1/A" => RegistrationS1,
455
+ "F-1" => RegistrationS1,
456
+ "F-1/A" => RegistrationS1,
457
+ "S-3" => RegistrationS3,
458
+ "S-3/A" => RegistrationS3,
459
+ "S-3ASR" => RegistrationS3,
460
+ "S-3ASR/A" => RegistrationS3,
461
+ "S-3D" => RegistrationS3,
462
+ "S-3DPOS" => RegistrationS3,
463
+ "F-3" => RegistrationS3,
464
+ "F-3/A" => RegistrationS3,
465
+ "F-3ASR" => RegistrationS3,
466
+ "F-3ASR/A" => RegistrationS3,
467
+ "424B1" => Prospectus424B,
468
+ "424B2" => Prospectus424B,
469
+ "424B3" => Prospectus424B,
470
+ "424B4" => Prospectus424B,
471
+ "424B5" => Prospectus424B,
472
+ "424B7" => Prospectus424B,
473
+ "424B8" => Prospectus424B,
474
+ "497K" => Prospectus497K,
475
+ "N-CSR" => FundShareholderReport,
476
+ "N-CSRS" => FundShareholderReport,
477
+ "N-CEN" => FundCensus,
478
+ "N-MFP2" => MoneyMarketFund,
479
+ "N-MFP3" => MoneyMarketFund,
480
+ "N-PX" => NPX,
481
+ "10-D" => TenD,
482
+ "10-D/A" => TenD,
483
+ "24F-2NT" => FundFeeNotice,
484
+ "MA-I" => MunicipalAdvisorForm,
485
+ "MA" => XmlFiling,
486
+ "MA-W" => XmlFiling,
487
+ "C" => FormC,
488
+ "C/A" => FormC,
489
+ "C-U" => FormC,
490
+ "C-AR" => FormC,
491
+ "C-TR" => FormC,
492
+ "D" => FormD,
493
+ "D/A" => FormD,
494
+ "CORRESP" => Correspondence,
495
+ "UPLOAD" => Correspondence,
496
+ "EFFECT" => Effect,
497
+ "ATS-N" => AlternativeTradingSystem,
498
+ "ATS-N/A" => AlternativeTradingSystem,
499
+ "ATS-N-W" => AlternativeTradingSystem,
500
+ "DRS" => DraftRegistrationStatement,
501
+ "DRS/A" => DraftRegistrationStatement,
502
+ "X-17A-5" => XmlFiling,
503
+ "TA-1" => XmlFiling,
504
+ "TA-2" => XmlFiling,
505
+ "TA-W" => XmlFiling,
506
+ "CFPORTAL" => XmlFiling,
507
+ "SBSE" => XmlFiling,
508
+ "SBSE-A" => XmlFiling,
509
+ "SBSE-W" => XmlFiling,
510
+ "ATS-N-C" => AtsCessation
511
+ }.freeze
512
+
513
+ def self.for(filing)
514
+ form = filing.form
515
+ klass = FORM_MAP[form]
516
+ klass ||= FORM_MAP[form.sub(%r{/A$}, "")]
517
+ klass ||= Base
518
+ klass.new(filing)
519
+ end
520
+ end
521
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Edgar
4
+ # Base class wrapping SEC submissions API data for a CIK entity.
5
+ class Entity
6
+ attr_reader :cik
7
+
8
+ def initialize(cik:, name: nil)
9
+ @cik = cik.to_i
10
+ @name = name
11
+ @entity_data = nil
12
+ end
13
+
14
+ def name
15
+ @name || data.name
16
+ end
17
+
18
+ def data
19
+ @data ||= EntityData.new(cik: @cik)
20
+ end
21
+
22
+ def display_name
23
+ data.name
24
+ end
25
+
26
+ def company?
27
+ data.company?
28
+ end
29
+
30
+ def individual?
31
+ data.individual?
32
+ end
33
+
34
+ def not_found?
35
+ data.not_found?
36
+ end
37
+
38
+ def mailing_address
39
+ data.mailing_address
40
+ end
41
+
42
+ def business_address
43
+ data.business_address
44
+ end
45
+
46
+ def get_filings(form: nil, year: nil, quarter: nil, accession_number: nil,
47
+ filing_date: nil, date: nil,
48
+ amendments: nil, date_start: nil, date_end: nil)
49
+ filings = if year && quarter
50
+ Filings.from_quarter(year, quarter)
51
+ else
52
+ build_filings_from_submissions
53
+ end
54
+
55
+ filings = filings.filter(
56
+ form: form,
57
+ filing_date: filing_date || date,
58
+ cik: @cik,
59
+ amendments: amendments,
60
+ date_start: date_start,
61
+ date_end: date_end
62
+ )
63
+
64
+ if accession_number
65
+ filing = filings.find { |f| f.accession_number == accession_number }
66
+ return filing ? Filings.new([filing]) : Filings.new([])
67
+ end
68
+
69
+ filings
70
+ end
71
+
72
+ def to_s
73
+ "#<Entity #{@cik}>"
74
+ end
75
+
76
+ private
77
+
78
+ def build_filings_from_submissions
79
+ all = data.all_filings
80
+ forms = all["form"] || []
81
+ dates = all["filingDate"] || []
82
+ accs = all["accessionNumber"] || []
83
+ companies = all["company"] || []
84
+
85
+ entries = forms.each_with_index.map do |f, i|
86
+ {
87
+ cik: @cik,
88
+ company: companies[i] || display_name,
89
+ form: f,
90
+ filing_date: dates[i],
91
+ accession_number: accs[i]
92
+ }
93
+ end
94
+
95
+ Filings.new(entries)
96
+ end
97
+ end
98
+ end