thirteen_f 0.2.7 → 0.5.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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/Gemfile +0 -2
- data/README.md +30 -23
- data/lib/thirteen_f.rb +3 -1
- data/lib/thirteen_f/cusip_securities.rb +3 -2
- data/lib/thirteen_f/entity.rb +105 -0
- data/lib/thirteen_f/filing.rb +25 -90
- data/lib/thirteen_f/position.rb +5 -14
- data/lib/thirteen_f/search.rb +10 -26
- data/lib/thirteen_f/search_hit.rb +31 -0
- data/lib/thirteen_f/sec_request.rb +44 -0
- data/lib/thirteen_f/version.rb +1 -1
- data/thirteen_f.gemspec +11 -10
- metadata +23 -21
- data/lib/thirteen_f/company.rb +0 -86
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2eebea964d2903739ae01ced769bfc61a0df3df5115d027760ff41fa348573a1
|
|
4
|
+
data.tar.gz: 2b0a2f7f48a25d1eed4238acdb6371333c4b85435f06880dcfdaf87087454ecd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 55ad096c2c2100ef187a154f1def213a643317ec42cfa7146f69b1fccd7d336a2c1445cb1e1a01f2dfc6c7d14875a430af627dad16f8c83c972c991c72756035
|
|
7
|
+
data.tar.gz: 9745d3a78b459759d50933bd2ee5c167bf4b28a0fc7991aae8e8d38ec187b9a6d600a864b7f10d186a147354fde62a1306ee59ed7be92e7d3548d4c30598f28d
|
data/.gitignore
CHANGED
data/Gemfile
CHANGED
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
|
|
@@ -116,10 +123,10 @@ git commits and tags, and push the `.gem` file to
|
|
|
116
123
|
## Contributing
|
|
117
124
|
|
|
118
125
|
Bug reports and pull requests are welcome on GitHub at
|
|
119
|
-
https://github.com/
|
|
126
|
+
https://github.com/savfischer/thirteen_f. This project is intended to be a safe,
|
|
120
127
|
welcoming space for collaboration, and contributors are expected to adhere to
|
|
121
128
|
the [code of
|
|
122
|
-
conduct](https://github.com/
|
|
129
|
+
conduct](https://github.com/savfischer/thirteen_f/blob/master/CODE_OF_CONDUCT.md).
|
|
123
130
|
|
|
124
131
|
|
|
125
132
|
## License
|
|
@@ -131,4 +138,4 @@ License](https://opensource.org/licenses/MIT).
|
|
|
131
138
|
|
|
132
139
|
Everyone interacting in the ThirteenF project's codebases, issue trackers, chat
|
|
133
140
|
rooms and mailing lists is expected to follow the [code of
|
|
134
|
-
conduct](https://github.com/
|
|
141
|
+
conduct](https://github.com/savfischer/thirteen_f/blob/master/CODE_OF_CONDUCT.md).
|
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"
|
|
@@ -48,10 +48,11 @@ class ThirteenF
|
|
|
48
48
|
reader.pages[2..-1].each do |page|
|
|
49
49
|
lines = page.text.split("\n").reject(&:empty?)[3..-1]
|
|
50
50
|
line_arrs = lines.map do |line|
|
|
51
|
-
next nil if line.include?('Total
|
|
52
|
-
line.split(
|
|
51
|
+
next nil if line.include?('Total Coun')
|
|
52
|
+
line.split(/\s{3}|( \* )/).reject(&:empty?).map(&:strip).reject { |text| text == '*' }
|
|
53
53
|
end
|
|
54
54
|
line_arrs.compact.each do |line_arr|
|
|
55
|
+
next unless line_arr.count > 1
|
|
55
56
|
valid_entries.push ListEntry.new(line_arr)
|
|
56
57
|
end
|
|
57
58
|
end
|
|
@@ -0,0 +1,105 @@
|
|
|
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
|
+
def sec_filings_page_url
|
|
49
|
+
"#{BASE_URL}/cgi-bin/browse-edgar?CIK=#{cik}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def thirteen_f_filings_url(count: 10)
|
|
53
|
+
"#{sec_filings_page_url}&type=13f&count=#{count}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.sec_url_from_cik(cik)
|
|
57
|
+
"#{BASE_URL}/cgi-bin/browse-edgar?CIK=#{cik}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def thirteen_f_urls(count: 10)
|
|
61
|
+
response = HTTP.get thirteen_f_filings_url(count: count)
|
|
62
|
+
page = Nokogiri::HTML response.to_s
|
|
63
|
+
page.search('#documentsbutton').map do |btn|
|
|
64
|
+
next nil unless btn.parent.previous.previous.text.include?('13F-HR')
|
|
65
|
+
"#{BASE_URL + btn.attributes['href'].value}"
|
|
66
|
+
end.compact
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
def cik_from_id(id)
|
|
71
|
+
id.prepend('0') until id.length >= 10
|
|
72
|
+
id
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def thirteen_f_filing_data(filings_data)
|
|
76
|
+
indexes = thirteen_f_indexes(filings_data)
|
|
77
|
+
indexes.map do |index|
|
|
78
|
+
columnar_data = [
|
|
79
|
+
filings_data[:accessionNumber][index],
|
|
80
|
+
filings_data[:filingDate][index],
|
|
81
|
+
filings_data[:reportDate][index],
|
|
82
|
+
filings_data[:acceptanceDateTime][index],
|
|
83
|
+
filings_data[:act][index],
|
|
84
|
+
filings_data[:form][index],
|
|
85
|
+
filings_data[:fileNumber][index],
|
|
86
|
+
filings_data[:filmNumber][index],
|
|
87
|
+
filings_data[:items][index],
|
|
88
|
+
filings_data[:size][index],
|
|
89
|
+
filings_data[:isXBRL][index],
|
|
90
|
+
filings_data[:isInlineXBRL][index],
|
|
91
|
+
filings_data[:primaryDocument][index],
|
|
92
|
+
filings_data[:primaryDocDescription][index]
|
|
93
|
+
]
|
|
94
|
+
Filing.new self, columnar_data
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def thirteen_f_indexes(filings_data)
|
|
99
|
+
filings_data[:form].each_with_index.map do |form, i|
|
|
100
|
+
form.start_with?('13F') ? i : nil
|
|
101
|
+
end.compact
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
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,
|
|
@@ -10,20 +8,13 @@ class ThirteenF
|
|
|
10
8
|
|
|
11
9
|
def self.from_xml_filing(filing)
|
|
12
10
|
return nil unless filing.table_xml_url
|
|
13
|
-
|
|
14
|
-
xml_doc = Nokogiri::XML response.to_s
|
|
15
|
-
xml_doc.search('infoTable').map do |info_table|
|
|
16
|
-
position = new filing: filing
|
|
17
|
-
position.attributes_from_info_table(info_table)
|
|
18
|
-
position
|
|
19
|
-
end
|
|
11
|
+
from_xml_url(filing.table_xml_url, filing: filing)
|
|
20
12
|
end
|
|
21
13
|
|
|
22
|
-
def self.from_xml_url(table_xml_url)
|
|
23
|
-
|
|
24
|
-
xml_doc
|
|
25
|
-
|
|
26
|
-
position = new
|
|
14
|
+
def self.from_xml_url(table_xml_url, filing: nil)
|
|
15
|
+
xml_doc = SecRequest.get table_xml_url, response_type: :xml
|
|
16
|
+
xml_doc.search("//infoTable").map do |info_table|
|
|
17
|
+
position = new filing: filing
|
|
27
18
|
position.attributes_from_info_table(info_table)
|
|
28
19
|
position
|
|
29
20
|
end
|
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,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class ThirteenF
|
|
4
|
+
class SearchHit
|
|
5
|
+
attr_reader :cik, :name, :entity
|
|
6
|
+
|
|
7
|
+
def self.from_search_hits(hits)
|
|
8
|
+
hits.map { |hit| new hit }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize(sec_hit)
|
|
12
|
+
@cik = cik_from_id sec_hit[:_id]
|
|
13
|
+
@name = sec_hit[:_source][:entity]
|
|
14
|
+
true
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def get_entity
|
|
18
|
+
entity_url = "https://data.sec.gov/submissions/CIK#{cik}.json"
|
|
19
|
+
response = SecRequest.get entity_url
|
|
20
|
+
@entity = Entity.new response
|
|
21
|
+
true
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
def cik_from_id(id)
|
|
26
|
+
id.prepend('0') until id.length >= 10
|
|
27
|
+
id
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
require 'http'
|
|
2
|
+
require 'nokogiri'
|
|
3
|
+
|
|
4
|
+
class ThirteenF
|
|
5
|
+
class SecRequest
|
|
6
|
+
HEADERS = {
|
|
7
|
+
'User-Agent' => 'ThirteenF/v0.5.0 (Open Source Ruby Gem)',
|
|
8
|
+
'Accept-Encoding' => 'gzip, deflate',
|
|
9
|
+
'Accept' => '*/*'
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
def self.get(url, response_type: :json)
|
|
13
|
+
response = HTTP.use(:auto_inflate).headers(HEADERS).get(url)
|
|
14
|
+
handle_response response, response_type: response_type
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.post(url, json)
|
|
18
|
+
response = HTTP.use(:auto_inflate).headers(HEADERS).post(url, json: json)
|
|
19
|
+
handle_response response
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.handle_response(response, response_type: :json)
|
|
23
|
+
case response.status
|
|
24
|
+
when 200, 201, 202, 203, 204, 206
|
|
25
|
+
handle_response_type response.to_s, response_type
|
|
26
|
+
else
|
|
27
|
+
raise "Request failed with response #{response.status}, request url: #{response.uri.to_s}"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.handle_response_type(body, response_type)
|
|
32
|
+
case response_type
|
|
33
|
+
when :html
|
|
34
|
+
Nokogiri::HTML body
|
|
35
|
+
when :json
|
|
36
|
+
JSON.parse body, symbolize_names: true
|
|
37
|
+
when :xml
|
|
38
|
+
xml_doc = Nokogiri::XML body
|
|
39
|
+
xml_doc.remove_namespaces!
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
data/lib/thirteen_f/version.rb
CHANGED
data/thirteen_f.gemspec
CHANGED
|
@@ -3,8 +3,8 @@ 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 = ["
|
|
7
|
-
spec.email = ["
|
|
6
|
+
spec.authors = ["Savannah Fischer"]
|
|
7
|
+
spec.email = ["savannah.fischer@hey.com"]
|
|
8
8
|
|
|
9
9
|
spec.summary = %q{A ruby interface for S.E.C. 13F Data.}
|
|
10
10
|
|
|
@@ -12,14 +12,14 @@ 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.}
|
|
16
|
-
spec.homepage = "https://github.com/
|
|
15
|
+
different investors have been doing in US regulated equity markets.}
|
|
16
|
+
spec.homepage = "https://github.com/savfischer/thirteen_f"
|
|
17
17
|
spec.license = "MIT"
|
|
18
|
-
spec.required_ruby_version = Gem::Requirement.new(">= 2.
|
|
18
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 2.6.0")
|
|
19
19
|
|
|
20
20
|
spec.metadata["homepage_uri"] = spec.homepage
|
|
21
|
-
spec.metadata["source_code_uri"] = "https://github.com/
|
|
22
|
-
spec.metadata["changelog_uri"] = "https://github.com/
|
|
21
|
+
spec.metadata["source_code_uri"] = "https://github.com/savfischer/thirteen_f"
|
|
22
|
+
spec.metadata["changelog_uri"] = "https://github.com/savfischer/thirteen_f/commits/master"
|
|
23
23
|
|
|
24
24
|
# Specify which files should be added to the gem when it is released.
|
|
25
25
|
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
@@ -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.10"
|
|
38
|
+
spec.add_runtime_dependency "pdf-reader", ">= 2.2"
|
|
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.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
|
-
-
|
|
8
|
-
autorequire:
|
|
7
|
+
- Savannah Fischer
|
|
8
|
+
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2021-07-03 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: minitest
|
|
@@ -44,50 +44,50 @@ dependencies:
|
|
|
44
44
|
requirements:
|
|
45
45
|
- - ">="
|
|
46
46
|
- !ruby/object:Gem::Version
|
|
47
|
-
version: '
|
|
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: '
|
|
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.10'
|
|
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.10'
|
|
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.2'
|
|
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.2'
|
|
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: []
|
|
92
92
|
extensions: []
|
|
93
93
|
extra_rdoc_files: []
|
|
@@ -103,22 +103,24 @@ 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
|
-
homepage: https://github.com/
|
|
116
|
+
homepage: https://github.com/savfischer/thirteen_f
|
|
115
117
|
licenses:
|
|
116
118
|
- MIT
|
|
117
119
|
metadata:
|
|
118
|
-
homepage_uri: https://github.com/
|
|
119
|
-
source_code_uri: https://github.com/
|
|
120
|
-
changelog_uri: https://github.com/
|
|
121
|
-
post_install_message:
|
|
120
|
+
homepage_uri: https://github.com/savfischer/thirteen_f
|
|
121
|
+
source_code_uri: https://github.com/savfischer/thirteen_f
|
|
122
|
+
changelog_uri: https://github.com/savfischer/thirteen_f/commits/master
|
|
123
|
+
post_install_message:
|
|
122
124
|
rdoc_options: []
|
|
123
125
|
require_paths:
|
|
124
126
|
- lib
|
|
@@ -126,15 +128,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
126
128
|
requirements:
|
|
127
129
|
- - ">="
|
|
128
130
|
- !ruby/object:Gem::Version
|
|
129
|
-
version: 2.
|
|
131
|
+
version: 2.6.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.
|
|
137
|
-
signing_key:
|
|
138
|
+
rubygems_version: 3.2.15
|
|
139
|
+
signing_key:
|
|
138
140
|
specification_version: 4
|
|
139
141
|
summary: A ruby interface for S.E.C. 13F Data.
|
|
140
142
|
test_files: []
|
data/lib/thirteen_f/company.rb
DELETED
|
@@ -1,86 +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: 100)
|
|
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
|
-
"#{BASE_URL + btn.attributes['href'].value}"
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
private
|
|
81
|
-
def self.parse_name(name_cell)
|
|
82
|
-
name_cell.text.split("\n").first
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
|