thirteen_f 0.4.0 → 0.5.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/Gemfile.lock +13 -15
- data/README.md +27 -20
- data/lib/thirteen_f/cusip_securities.rb +2 -7
- data/lib/thirteen_f/entity.rb +88 -0
- data/lib/thirteen_f/filing.rb +25 -90
- data/lib/thirteen_f/position.rb +1 -5
- data/lib/thirteen_f/search.rb +10 -26
- data/lib/thirteen_f/search_hit.rb +34 -0
- data/lib/thirteen_f/sec_request.rb +61 -0
- data/lib/thirteen_f/version.rb +1 -1
- data/lib/thirteen_f.rb +3 -1
- data/thirteen_f.gemspec +7 -6
- metadata +15 -13
- data/lib/thirteen_f/company.rb +0 -87
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 89b2921abd3ae773a2d34c3ddb1551eea71708ecaa4218608a520e84a5e3120d
|
4
|
+
data.tar.gz: abc9e19a98b164ce1b72a6da225639209b09cef3837ae2ce33f4f9c9c8b96290
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8fa4cd7362ab9fb3e68b4e1b1657d9de45e763e5d75eb76327391a84c87904d86dfdb65cfa51b9424fdf1215b0e360058e50cbeb738308048f9f0a8304b0c79a
|
7
|
+
data.tar.gz: ae1ed7ffca8fb036fbff8cd7419e6f9ef656063587322e23936c682ebdbe324375203c3b9a1d87ee88c9bb83cfedc7af88ad5191da303d6d773d61b500261653
|
data/.gitignore
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,44 +1,42 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
thirteen_f (0.
|
5
|
-
http (>= 5)
|
6
|
-
nokogiri
|
7
|
-
pdf-reader
|
4
|
+
thirteen_f (0.5.1)
|
5
|
+
http (>= 5.0)
|
6
|
+
nokogiri (>= 1.10)
|
7
|
+
pdf-reader (>= 2.2)
|
8
8
|
|
9
9
|
GEM
|
10
10
|
remote: https://rubygems.org/
|
11
11
|
specs:
|
12
12
|
Ascii85 (1.1.0)
|
13
|
-
addressable (2.
|
13
|
+
addressable (2.8.0)
|
14
14
|
public_suffix (>= 2.0.2, < 5.0)
|
15
15
|
afm (0.2.2)
|
16
16
|
coderay (1.1.3)
|
17
17
|
domain_name (0.5.20190701)
|
18
18
|
unf (>= 0.0.5, < 1.0.0)
|
19
|
-
ffi (1.15.
|
19
|
+
ffi (1.15.3)
|
20
20
|
ffi-compiler (1.0.1)
|
21
21
|
ffi (>= 1.0.0)
|
22
22
|
rake
|
23
23
|
hashery (2.1.2)
|
24
|
-
http (5.0.
|
24
|
+
http (5.0.1)
|
25
25
|
addressable (~> 2.3)
|
26
26
|
http-cookie (~> 1.0)
|
27
27
|
http-form_data (~> 2.2)
|
28
|
-
llhttp-ffi (~> 0.0
|
29
|
-
http-cookie (1.0.
|
28
|
+
llhttp-ffi (~> 0.3.0)
|
29
|
+
http-cookie (1.0.4)
|
30
30
|
domain_name (~> 0.5)
|
31
31
|
http-form_data (2.3.0)
|
32
|
-
llhttp-ffi (0.
|
32
|
+
llhttp-ffi (0.3.1)
|
33
33
|
ffi-compiler (~> 1.0)
|
34
34
|
rake (~> 13.0)
|
35
35
|
method_source (1.0.0)
|
36
|
-
mini_portile2 (2.5.1)
|
37
36
|
minitest (5.14.4)
|
38
|
-
nokogiri (1.11.
|
39
|
-
mini_portile2 (~> 2.5.0)
|
37
|
+
nokogiri (1.11.7-x86_64-darwin)
|
40
38
|
racc (~> 1.4)
|
41
|
-
pdf-reader (2.
|
39
|
+
pdf-reader (2.5.0)
|
42
40
|
Ascii85 (~> 1.0)
|
43
41
|
afm (~> 0.2.1)
|
44
42
|
hashery (~> 2.0)
|
@@ -65,4 +63,4 @@ DEPENDENCIES
|
|
65
63
|
thirteen_f!
|
66
64
|
|
67
65
|
BUNDLED WITH
|
68
|
-
2.
|
66
|
+
2.2.15
|
data/README.md
CHANGED
@@ -39,40 +39,38 @@ Or install it yourself as:
|
|
39
39
|
|
40
40
|
```ruby
|
41
41
|
search = ThirteenF::Search.new('Berkshire Hathaway')
|
42
|
-
search.
|
43
|
-
|
42
|
+
search.get_results
|
43
|
+
|
44
|
+
result = search.results.first
|
45
|
+
result.get_entity
|
46
|
+
entity = result.entity
|
44
47
|
```
|
45
48
|
|
46
|
-
###
|
49
|
+
### Entities
|
47
50
|
|
48
51
|
```ruby
|
49
52
|
cik_number = '0001061768'
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
company.cik # type: String | ex: "0001067983"
|
60
|
-
company.name # type: String | ex: "BERKSHIRE HATHAWAY INC"
|
61
|
-
company.state_or_country # type: String | ex: "NE"
|
53
|
+
entity = ThirteenF::Entity.from_cik cik_number
|
54
|
+
|
55
|
+
entity.most_recent_filing
|
56
|
+
entity.get_most_recent_positions
|
57
|
+
entity.most_recent_positions # returns positions from the most recent 13F filing
|
58
|
+
|
59
|
+
entity.cik # type: String | ex: "0001067983"
|
60
|
+
entity.name # type: String | ex: "BERKSHIRE HATHAWAY INC"
|
61
|
+
entity.state_or_country # type: String | ex: "NE"
|
62
62
|
```
|
63
63
|
|
64
64
|
### Filings
|
65
65
|
|
66
66
|
```ruby
|
67
|
-
|
68
|
-
filing
|
69
|
-
filing.company
|
67
|
+
filing = entity.filings.first
|
68
|
+
filing.entity
|
70
69
|
filing.get_positions
|
71
|
-
filing.positions # returns the US public securities held by the
|
70
|
+
filing.positions # returns the US public securities held by the entity at the
|
72
71
|
# time of the period of the report
|
73
72
|
|
74
73
|
filing.index_url # type: String
|
75
|
-
filing.response_status # type: String | ex: "200 OK"
|
76
74
|
filing.period_of_report # type: Date or nil
|
77
75
|
filing.time_accepted # type: DateTime or nil
|
78
76
|
filing.table_html_url # type: String or nil
|
@@ -101,6 +99,15 @@ position.other_managers # type: String or nil | ex: "1,4,11"
|
|
101
99
|
position.voting_authority # type: Hash | ex: { sole: 19994970, shared: 0, none: 0 }
|
102
100
|
```
|
103
101
|
|
102
|
+
### Net Securities
|
103
|
+
```ruby
|
104
|
+
```
|
105
|
+
|
106
|
+
### CUSIP Securities
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
```
|
110
|
+
|
104
111
|
## Development
|
105
112
|
|
106
113
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
@@ -1,6 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'http'
|
4
3
|
require 'date'
|
5
4
|
require 'open-uri'
|
6
5
|
require 'pdf-reader'
|
@@ -12,9 +11,7 @@ class ThirteenF
|
|
12
11
|
|
13
12
|
def self.all_file_locations
|
14
13
|
index_url = "#{BASE_URL}/divisions/investment/13flists.htm"
|
15
|
-
|
16
|
-
return false unless response.status == 200
|
17
|
-
page = Nokogiri::HTML response.to_s
|
14
|
+
page = SecRequest.get index_url, response_type: :html
|
18
15
|
a_tags = page.search('a').select do |a_tag|
|
19
16
|
href = a_tag.attributes['href']&.value.to_s
|
20
17
|
href.include?('13flist') && href.include?('.pdf')
|
@@ -24,9 +21,7 @@ class ThirteenF
|
|
24
21
|
|
25
22
|
def self.most_recent_list
|
26
23
|
index_url = "#{BASE_URL}/divisions/investment/13flists.htm"
|
27
|
-
|
28
|
-
return false unless response.status == 200
|
29
|
-
page = Nokogiri::HTML response.to_s
|
24
|
+
page = SecRequest.get index_url, response_type: :html
|
30
25
|
a_tag = page.search('a').find { |a| a.text.include?('Current List') }
|
31
26
|
file_location = "#{BASE_URL + a_tag.attributes['href'].value}"
|
32
27
|
new file_location
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'thirteen_f/sec_request'
|
4
|
+
|
5
|
+
class ThirteenF
|
6
|
+
class Entity
|
7
|
+
attr_reader :cik, :name, :tickers, :exchanges,
|
8
|
+
:sic, :sic_description,
|
9
|
+
:fiscal_year_end,
|
10
|
+
:business_state_or_country,
|
11
|
+
:filings, :most_recent_positions
|
12
|
+
|
13
|
+
BASE_URL = 'https://www.sec.gov'
|
14
|
+
|
15
|
+
def self.from_cik(cik)
|
16
|
+
entity_url = "https://data.sec.gov/submissions/CIK#{cik}.json"
|
17
|
+
new SecRequest.get entity_url
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(sec_entity)
|
21
|
+
@name = sec_entity[:name]
|
22
|
+
@cik = cik_from_id sec_entity[:cik]
|
23
|
+
@tickers = sec_entity[:tickers]
|
24
|
+
@exchanges = sec_entity[:exchanges]
|
25
|
+
@sic = sec_entity[:sic]
|
26
|
+
@sic_description = sec_entity[:sicDescription]
|
27
|
+
@fiscal_year_end = sec_entity[:fiscalYearEnd]
|
28
|
+
@business_state_or_country = sec_entity[:addresses][:business][:stateOrCountry]
|
29
|
+
@filings = thirteen_f_filing_data sec_entity[:filings][:recent]
|
30
|
+
true
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
def human_fiscal_year_end
|
35
|
+
Time.strptime(@fiscal_year_end, '%m%d').strftime('%B %d')
|
36
|
+
end
|
37
|
+
|
38
|
+
def get_most_recent_positions
|
39
|
+
most_recent_filing.get_positions
|
40
|
+
@most_recent_positions = most_recent_filing.positions
|
41
|
+
true
|
42
|
+
end
|
43
|
+
|
44
|
+
def most_recent_filing
|
45
|
+
filings.select(&:period_of_report).max_by(&:period_of_report)
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
def cik_from_id(id)
|
50
|
+
id.prepend('0') until id.length >= 10
|
51
|
+
id
|
52
|
+
end
|
53
|
+
|
54
|
+
COLUMN_KEYS = %i(
|
55
|
+
accessionNumber
|
56
|
+
filingDate
|
57
|
+
reportDate
|
58
|
+
acceptanceDateTime
|
59
|
+
act
|
60
|
+
form
|
61
|
+
fileNumber
|
62
|
+
filmNumber
|
63
|
+
items
|
64
|
+
size
|
65
|
+
isXBRL
|
66
|
+
isInlineXBRL
|
67
|
+
primaryDocument
|
68
|
+
primaryDocDescription
|
69
|
+
)
|
70
|
+
|
71
|
+
def thirteen_f_filing_data(filings_data)
|
72
|
+
indexes = thirteen_f_indexes(filings_data)
|
73
|
+
indexes.map do |index|
|
74
|
+
columnar_data = COLUMN_KEYS.map do |key|
|
75
|
+
filings_data[key][index]
|
76
|
+
end
|
77
|
+
Filing.new self, columnar_data
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def thirteen_f_indexes(filings_data)
|
82
|
+
filings_data[:form].each_with_index.map do |form, i|
|
83
|
+
form.start_with?('13F') ? i : nil
|
84
|
+
end.compact
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
data/lib/thirteen_f/filing.rb
CHANGED
@@ -1,80 +1,47 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'http'
|
4
3
|
require 'date'
|
5
4
|
|
6
5
|
class ThirteenF
|
7
6
|
class Filing
|
8
|
-
attr_reader :index_url, :
|
7
|
+
attr_reader :entity, :index_url, :report_date, :time_accepted, :form_type,
|
8
|
+
:table_html_url, :table_xml_url,
|
9
9
|
:cover_page_html_url, :cover_page_xml_url, :complete_text_file_url,
|
10
|
-
:
|
10
|
+
:positions
|
11
11
|
|
12
|
-
|
12
|
+
alias period_of_report report_date
|
13
13
|
|
14
|
-
|
15
|
-
redo_count = 0
|
16
|
-
urls.map do |index_url|
|
17
|
-
response = HTTP.get index_url
|
18
|
-
sleep 0.33
|
19
|
-
if response.status == 200
|
20
|
-
redo_count = 0
|
21
|
-
attributes = set_attributes(response, index_url, company)
|
22
|
-
new(**attributes)
|
23
|
-
else
|
24
|
-
redo_count += 1
|
25
|
-
redo unless redo_count > 1
|
26
|
-
attributes = bad_response_attributes(response, index_url, company)
|
27
|
-
new(**attributes)
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
14
|
+
BASE_URL = 'https://www.sec.gov'
|
31
15
|
|
32
|
-
def
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
true
|
40
|
-
else
|
41
|
-
false
|
42
|
-
end
|
16
|
+
def initialize(entity, columnar_data)
|
17
|
+
@entity = entity
|
18
|
+
@index_url = assemble_index_url columnar_data[0]
|
19
|
+
@report_date = Date.parse columnar_data[2]
|
20
|
+
@time_accepted = DateTime.parse columnar_data[3]
|
21
|
+
@form_type = columnar_data[5]
|
22
|
+
true
|
43
23
|
end
|
44
24
|
|
45
|
-
def
|
46
|
-
|
47
|
-
table_html_url: nil, table_xml_url: nil,
|
48
|
-
cover_page_html_url: nil, cover_page_xml_url: nil)
|
49
|
-
@company = company
|
50
|
-
@response_status = response_status
|
51
|
-
@index_url = index_url
|
52
|
-
@table_html_url = table_html_url
|
53
|
-
@table_xml_url = table_xml_url
|
54
|
-
@cover_page_html_url = cover_page_html_url
|
55
|
-
@cover_page_xml_url = cover_page_xml_url
|
56
|
-
@complete_text_file_url = complete_text_file_url
|
57
|
-
@period_of_report = period_of_report
|
58
|
-
@time_accepted = time_accepted
|
59
|
-
true
|
25
|
+
def assemble_index_url(accession_number)
|
26
|
+
"#{BASE_URL}/Archives/edgar/data/#{entity.cik}/#{accession_number.delete('-')}/#{accession_number}-index.htm"
|
60
27
|
end
|
61
28
|
|
62
29
|
def get_positions
|
63
|
-
|
30
|
+
set_attributes_from_index_url unless table_xml_url
|
64
31
|
@positions = Position.from_xml_filing self
|
65
32
|
true
|
66
33
|
end
|
67
34
|
|
35
|
+
def set_attributes_from_index_url
|
36
|
+
return unless index_url
|
37
|
+
response = SecRequest.get index_url, response_type: :html
|
38
|
+
assign_attributes(**set_attributes(response))
|
39
|
+
end
|
40
|
+
|
68
41
|
private
|
69
|
-
def
|
70
|
-
page = Nokogiri::HTML response.to_s
|
42
|
+
def set_attributes(page)
|
71
43
|
table_links = page.search('table.tableFile')[0].search('a')
|
72
44
|
attributes = Hash.new
|
73
|
-
attributes[:company] = company
|
74
|
-
attributes[:response_status] = response.status.to_s
|
75
|
-
attributes[:index_url] = index_url
|
76
|
-
attributes[:period_of_report] = get_period_of_report page
|
77
|
-
attributes[:time_accepted] = get_time_accepted page
|
78
45
|
attributes[:complete_text_file_url] = "#{BASE_URL + table_links[-1].attributes['href'].value}"
|
79
46
|
if table_links.count == 5
|
80
47
|
attributes = xml_present(attributes, table_links)
|
@@ -82,23 +49,7 @@ class ThirteenF
|
|
82
49
|
attributes
|
83
50
|
end
|
84
51
|
|
85
|
-
def
|
86
|
-
period_header_div = page.search('div.infoHead').find do |div|
|
87
|
-
div.text.include?('Period of Report')
|
88
|
-
end
|
89
|
-
period_string = period_header_div.next.next.text.strip
|
90
|
-
Date.parse period_string
|
91
|
-
end
|
92
|
-
|
93
|
-
def self.get_time_accepted(page)
|
94
|
-
accepted_header_div = page.search('div.infoHead').find do |div|
|
95
|
-
div.text.include?('Accepted')
|
96
|
-
end
|
97
|
-
accepted_string = accepted_header_div.next.next.text.strip
|
98
|
-
DateTime.parse accepted_string
|
99
|
-
end
|
100
|
-
|
101
|
-
def self.xml_present(attributes, table_links)
|
52
|
+
def xml_present(attributes, table_links)
|
102
53
|
attributes[:table_html_url] = "#{BASE_URL + table_links[2].attributes['href'].value}"
|
103
54
|
attributes[:table_xml_url] = "#{BASE_URL + table_links[3].attributes['href'].value}"
|
104
55
|
attributes[:cover_page_html_url] = "#{BASE_URL + table_links[0].attributes['href'].value}"
|
@@ -106,30 +57,14 @@ class ThirteenF
|
|
106
57
|
attributes
|
107
58
|
end
|
108
59
|
|
109
|
-
def
|
110
|
-
|
111
|
-
attributes[:company] = company
|
112
|
-
attributes[:response_status] = response.status.to_s
|
113
|
-
attributes[:index_url] = index_url
|
114
|
-
attributes[:period_of_report] = nil
|
115
|
-
attributes[:time_accepted] = nil
|
116
|
-
attributes[:complete_text_file_url] = nil
|
117
|
-
attributes
|
118
|
-
end
|
119
|
-
|
120
|
-
def assign_attributes(response_status:, index_url:, company:,
|
121
|
-
complete_text_file_url:, period_of_report:,
|
122
|
-
time_accepted:, table_html_url: nil, table_xml_url:
|
123
|
-
nil, cover_page_html_url: nil,
|
60
|
+
def assign_attributes(complete_text_file_url:, table_html_url: nil,
|
61
|
+
table_xml_url: nil, cover_page_html_url: nil,
|
124
62
|
cover_page_xml_url: nil)
|
125
|
-
@response_status = response_status
|
126
63
|
@table_html_url = table_html_url
|
127
64
|
@table_xml_url = table_xml_url
|
128
65
|
@cover_page_html_url = cover_page_html_url
|
129
66
|
@cover_page_xml_url = cover_page_xml_url
|
130
67
|
@complete_text_file_url = complete_text_file_url
|
131
|
-
@period_of_report = period_of_report
|
132
|
-
@time_accepted = time_accepted
|
133
68
|
true
|
134
69
|
end
|
135
70
|
end
|
data/lib/thirteen_f/position.rb
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'http'
|
4
|
-
|
5
3
|
class ThirteenF
|
6
4
|
class Position
|
7
5
|
attr_reader :name_of_issuer, :title_of_class, :cusip, :value_in_thousands,
|
@@ -14,9 +12,7 @@ class ThirteenF
|
|
14
12
|
end
|
15
13
|
|
16
14
|
def self.from_xml_url(table_xml_url, filing: nil)
|
17
|
-
|
18
|
-
xml_doc = Nokogiri::XML response.to_s
|
19
|
-
xml_doc.remove_namespaces!
|
15
|
+
xml_doc = SecRequest.get table_xml_url, response_type: :xml
|
20
16
|
xml_doc.search("//infoTable").map do |info_table|
|
21
17
|
position = new filing: filing
|
22
18
|
position.attributes_from_info_table(info_table)
|
data/lib/thirteen_f/search.rb
CHANGED
@@ -1,42 +1,26 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'http'
|
4
|
-
require 'nokogiri'
|
5
|
-
|
6
3
|
class ThirteenF
|
7
4
|
class Search
|
8
|
-
attr_reader :
|
5
|
+
attr_reader :results, :search_params
|
9
6
|
|
10
|
-
|
11
|
-
SEARCH_URL = "#{BASE_URL}/cgi-bin/browse-edgar"
|
7
|
+
SEARCH_URL = 'https://efts.sec.gov/LATEST/search-index'
|
12
8
|
|
13
|
-
def initialize(search_string
|
14
|
-
@search_params = [SEARCH_URL,
|
15
|
-
company: search_string,
|
16
|
-
count: count
|
17
|
-
}]
|
9
|
+
def initialize(search_string)
|
10
|
+
@search_params = [SEARCH_URL, { keysTyped: search_string, narrow: true }]
|
18
11
|
end
|
19
12
|
|
20
|
-
def
|
21
|
-
response =
|
22
|
-
|
23
|
-
@companies = configure_search_results response
|
24
|
-
else
|
25
|
-
raise 'SEC results are not available right now'
|
26
|
-
end
|
13
|
+
def get_results
|
14
|
+
response = SecRequest.post(*search_params)
|
15
|
+
@results = configure_search_results response
|
27
16
|
true
|
28
17
|
end
|
29
18
|
|
30
19
|
private
|
31
20
|
def configure_search_results(response)
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
if column_count == 3
|
36
|
-
company_rows.search('br').each { |n| n.replace("\n") }
|
37
|
-
Company.from_sec_search_rows company_rows
|
38
|
-
elsif column_count == 5
|
39
|
-
Company.from_company_page page
|
21
|
+
if response.dig(:hits, :hits)
|
22
|
+
SearchHit.from_search_hits response.dig(:hits, :hits)
|
23
|
+
else []
|
40
24
|
end
|
41
25
|
end
|
42
26
|
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class ThirteenF
|
4
|
+
class SearchHit
|
5
|
+
attr_reader :cik, :name, :entity, :sec_page_url
|
6
|
+
|
7
|
+
def self.from_search_hits(hits)
|
8
|
+
hits.map { |hit| new hit }
|
9
|
+
end
|
10
|
+
|
11
|
+
BASE_URL = 'https://www.sec.gov'
|
12
|
+
|
13
|
+
def initialize(sec_hit)
|
14
|
+
@cik = cik_from_id sec_hit[:_id]
|
15
|
+
@name = sec_hit[:_source][:entity]
|
16
|
+
@sec_page_url = "#{BASE_URL}/edgar/browse/?CIK=#{cik}&owner=exclude"
|
17
|
+
true
|
18
|
+
end
|
19
|
+
|
20
|
+
def get_entity
|
21
|
+
entity_url = "https://data.sec.gov/submissions/CIK#{cik}.json"
|
22
|
+
response = SecRequest.get entity_url
|
23
|
+
@entity = Entity.new response
|
24
|
+
true
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
def cik_from_id(id)
|
29
|
+
id.prepend('0') until id.length >= 10
|
30
|
+
id
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'http'
|
2
|
+
require 'nokogiri'
|
3
|
+
|
4
|
+
class ThirteenF
|
5
|
+
class SecRequest
|
6
|
+
DATA_HEADERS = {
|
7
|
+
'User-Agent' => 'ThirteenF/v0.5.0 (Open Source Ruby Gem) savannah.fischer@hey.com',
|
8
|
+
'Host' => 'data.sec.gov',
|
9
|
+
'Accept-Encoding' => 'gzip, deflate'
|
10
|
+
}
|
11
|
+
|
12
|
+
WWW_HEADERS = {
|
13
|
+
'User-Agent' => 'ThirteenF/v0.5.0 (Open Source Ruby Gem) savannah.fischer@hey.com',
|
14
|
+
'Host' => 'www.sec.gov'
|
15
|
+
}
|
16
|
+
|
17
|
+
EFTS_HEADERS = {
|
18
|
+
'User-Agent' => 'S Fischer sfischer@fischercompany.com',
|
19
|
+
'Accept-Encoding' => 'gzip, deflate',
|
20
|
+
'Host' => 'efts.sec.gov'
|
21
|
+
}
|
22
|
+
|
23
|
+
def self.get(url, response_type: :json)
|
24
|
+
case response_type
|
25
|
+
when :json
|
26
|
+
response = HTTP.use(:auto_inflate).headers(DATA_HEADERS).get(url)
|
27
|
+
handle_response response, response_type: response_type
|
28
|
+
else
|
29
|
+
response = HTTP.use(:auto_inflate).headers(WWW_HEADERS).get(url)
|
30
|
+
handle_response response, response_type: response_type
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.post(url, json)
|
35
|
+
response = HTTP.use(:auto_inflate).headers(EFTS_HEADERS).post(url, json: json)
|
36
|
+
handle_response response
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.handle_response(response, response_type: :json)
|
40
|
+
case response.status
|
41
|
+
when 200, 201, 202, 203, 204, 206
|
42
|
+
handle_response_type response.to_s, response_type
|
43
|
+
else
|
44
|
+
raise "Request failed with response #{response.status}, request url: #{response.uri.to_s}"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.handle_response_type(body, response_type)
|
49
|
+
case response_type
|
50
|
+
when :html
|
51
|
+
Nokogiri::HTML body
|
52
|
+
when :json
|
53
|
+
JSON.parse body, symbolize_names: true
|
54
|
+
when :xml
|
55
|
+
xml_doc = Nokogiri::XML body
|
56
|
+
xml_doc.remove_namespaces!
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
data/lib/thirteen_f/version.rb
CHANGED
data/lib/thirteen_f.rb
CHANGED
@@ -1,7 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require 'thirteen_f/sec_request'
|
4
|
+
require "thirteen_f/entity"
|
4
5
|
require "thirteen_f/search"
|
6
|
+
require "thirteen_f/search_hit"
|
5
7
|
require "thirteen_f/filing"
|
6
8
|
require "thirteen_f/position"
|
7
9
|
require "thirteen_f/net_position"
|
data/thirteen_f.gemspec
CHANGED
@@ -3,7 +3,7 @@ require_relative 'lib/thirteen_f/version'
|
|
3
3
|
Gem::Specification.new do |spec|
|
4
4
|
spec.name = "thirteen_f"
|
5
5
|
spec.version = ThirteenF::VERSION
|
6
|
-
spec.authors = ["
|
6
|
+
spec.authors = ["Savannah Fischer"]
|
7
7
|
spec.email = ["savannah.fischer@hey.com"]
|
8
8
|
|
9
9
|
spec.summary = %q{A ruby interface for S.E.C. 13F Data.}
|
@@ -12,10 +12,10 @@ Gem::Specification.new do |spec|
|
|
12
12
|
filing data. The SEC is the U.S. Securities and Exchange Commission. 13F
|
13
13
|
filings are disclosures large institutional investors in public securites have
|
14
14
|
to provide and make public every quarter. It is a great way to follow what
|
15
|
-
different investors have been doing.}
|
15
|
+
different investors have been doing in US regulated equity markets.}
|
16
16
|
spec.homepage = "https://github.com/savfischer/thirteen_f"
|
17
17
|
spec.license = "MIT"
|
18
|
-
spec.required_ruby_version = Gem::Requirement.new(">=
|
18
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 3.0")
|
19
19
|
|
20
20
|
spec.metadata["homepage_uri"] = spec.homepage
|
21
21
|
spec.metadata["source_code_uri"] = "https://github.com/savfischer/thirteen_f"
|
@@ -32,7 +32,8 @@ Gem::Specification.new do |spec|
|
|
32
32
|
|
33
33
|
spec.add_development_dependency "minitest"
|
34
34
|
spec.add_development_dependency "pry"
|
35
|
-
|
36
|
-
spec.
|
37
|
-
spec.
|
35
|
+
|
36
|
+
spec.add_runtime_dependency "http", ">= 5.0"
|
37
|
+
spec.add_runtime_dependency "nokogiri", ">= 1.13"
|
38
|
+
spec.add_runtime_dependency "pdf-reader", ">= 2.10"
|
38
39
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: thirteen_f
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
|
-
-
|
7
|
+
- Savannah Fischer
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-06-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: minitest
|
@@ -44,48 +44,48 @@ dependencies:
|
|
44
44
|
requirements:
|
45
45
|
- - ">="
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: '5'
|
47
|
+
version: '5.0'
|
48
48
|
type: :runtime
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: '5'
|
54
|
+
version: '5.0'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: nokogiri
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
59
|
- - ">="
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version: '
|
61
|
+
version: '1.13'
|
62
62
|
type: :runtime
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
66
|
- - ">="
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version: '
|
68
|
+
version: '1.13'
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: pdf-reader
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
73
|
- - ">="
|
74
74
|
- !ruby/object:Gem::Version
|
75
|
-
version: '
|
75
|
+
version: '2.10'
|
76
76
|
type: :runtime
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
80
|
- - ">="
|
81
81
|
- !ruby/object:Gem::Version
|
82
|
-
version: '
|
82
|
+
version: '2.10'
|
83
83
|
description: |-
|
84
84
|
thirteen_f lets you easily search and retrieve SEC 13F
|
85
85
|
filing data. The SEC is the U.S. Securities and Exchange Commission. 13F
|
86
86
|
filings are disclosures large institutional investors in public securites have
|
87
87
|
to provide and make public every quarter. It is a great way to follow what
|
88
|
-
different investors have been doing.
|
88
|
+
different investors have been doing in US regulated equity markets.
|
89
89
|
email:
|
90
90
|
- savannah.fischer@hey.com
|
91
91
|
executables: []
|
@@ -103,12 +103,14 @@ files:
|
|
103
103
|
- bin/console
|
104
104
|
- bin/setup
|
105
105
|
- lib/thirteen_f.rb
|
106
|
-
- lib/thirteen_f/company.rb
|
107
106
|
- lib/thirteen_f/cusip_securities.rb
|
107
|
+
- lib/thirteen_f/entity.rb
|
108
108
|
- lib/thirteen_f/filing.rb
|
109
109
|
- lib/thirteen_f/net_position.rb
|
110
110
|
- lib/thirteen_f/position.rb
|
111
111
|
- lib/thirteen_f/search.rb
|
112
|
+
- lib/thirteen_f/search_hit.rb
|
113
|
+
- lib/thirteen_f/sec_request.rb
|
112
114
|
- lib/thirteen_f/version.rb
|
113
115
|
- thirteen_f.gemspec
|
114
116
|
homepage: https://github.com/savfischer/thirteen_f
|
@@ -126,14 +128,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
126
128
|
requirements:
|
127
129
|
- - ">="
|
128
130
|
- !ruby/object:Gem::Version
|
129
|
-
version:
|
131
|
+
version: '3.0'
|
130
132
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
131
133
|
requirements:
|
132
134
|
- - ">="
|
133
135
|
- !ruby/object:Gem::Version
|
134
136
|
version: '0'
|
135
137
|
requirements: []
|
136
|
-
rubygems_version: 3.
|
138
|
+
rubygems_version: 3.3.7
|
137
139
|
signing_key:
|
138
140
|
specification_version: 4
|
139
141
|
summary: A ruby interface for S.E.C. 13F Data.
|
data/lib/thirteen_f/company.rb
DELETED
@@ -1,87 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'http'
|
4
|
-
|
5
|
-
class ThirteenF
|
6
|
-
class Company
|
7
|
-
attr_reader :cik, :name, :state_or_country, :filings, :most_recent_holdings
|
8
|
-
|
9
|
-
BASE_URL = 'https://www.sec.gov'
|
10
|
-
|
11
|
-
def self.from_sec_search_rows(rows)
|
12
|
-
rows.map do |row|
|
13
|
-
raise "Bad row" unless row.search('td').count == 3
|
14
|
-
cells = row.search('td')
|
15
|
-
cik = cells.first.text
|
16
|
-
name = parse_name cells[1]
|
17
|
-
state_or_country = cells.last.text
|
18
|
-
new cik, name, state_or_country
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
def self.from_company_page(page)
|
23
|
-
arr = page.search('.companyName').text.split('CIK#:')
|
24
|
-
name = arr.first.strip
|
25
|
-
cik = arr.last.strip.split(' ').first
|
26
|
-
state_or_country = page.search('.identInfo a').first.text
|
27
|
-
Array.new 1, new(cik, name, state_or_country)
|
28
|
-
end
|
29
|
-
|
30
|
-
def self.from_cik(cik)
|
31
|
-
response = HTTP.get sec_url_from_cik(cik)
|
32
|
-
return false unless response.status == 200
|
33
|
-
page = Nokogiri::HTML response.to_s
|
34
|
-
from_company_page(page).first
|
35
|
-
end
|
36
|
-
|
37
|
-
def initialize(cik, name, state_or_country)
|
38
|
-
@cik = cik
|
39
|
-
@name = name
|
40
|
-
@state_or_country = state_or_country
|
41
|
-
true
|
42
|
-
end
|
43
|
-
|
44
|
-
def get_most_recent_holdings
|
45
|
-
get_filings unless filings
|
46
|
-
most_recent_filing.get_positions
|
47
|
-
@most_recent_holdings = most_recent_filing.positions
|
48
|
-
true
|
49
|
-
end
|
50
|
-
|
51
|
-
def most_recent_filing
|
52
|
-
filings.select(&:period_of_report).max_by(&:period_of_report)
|
53
|
-
end
|
54
|
-
|
55
|
-
def get_filings(count: 10)
|
56
|
-
@filings = Filing.from_index_urls thirteen_f_urls(count: count), self
|
57
|
-
true
|
58
|
-
end
|
59
|
-
|
60
|
-
def sec_filings_page_url
|
61
|
-
"#{BASE_URL}/cgi-bin/browse-edgar?CIK=#{cik}"
|
62
|
-
end
|
63
|
-
|
64
|
-
def thirteen_f_filings_url(count: 10)
|
65
|
-
"#{sec_filings_page_url}&type=13f&count=#{count}"
|
66
|
-
end
|
67
|
-
|
68
|
-
def self.sec_url_from_cik(cik)
|
69
|
-
"#{BASE_URL}/cgi-bin/browse-edgar?CIK=#{cik}"
|
70
|
-
end
|
71
|
-
|
72
|
-
def thirteen_f_urls(count: 10)
|
73
|
-
response = HTTP.get thirteen_f_filings_url(count: count)
|
74
|
-
page = Nokogiri::HTML response.to_s
|
75
|
-
page.search('#documentsbutton').map do |btn|
|
76
|
-
next nil unless btn.parent.previous.previous.text.include?('13F-HR')
|
77
|
-
"#{BASE_URL + btn.attributes['href'].value}"
|
78
|
-
end.compact
|
79
|
-
end
|
80
|
-
|
81
|
-
private
|
82
|
-
def self.parse_name(name_cell)
|
83
|
-
name_cell.text.split("\n").first
|
84
|
-
end
|
85
|
-
end
|
86
|
-
end
|
87
|
-
|