sec_edgar 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/assets/1996/QTR1/master.idx +49935 -0
- data/lib/assets/1996/QTR2/master.idx +47669 -0
- data/lib/assets/1996/QTR3/master.idx +50651 -0
- data/lib/assets/1996/QTR4/master.idx +54399 -0
- data/lib/sec_edgar/address.rb +3 -0
- data/lib/sec_edgar/basic_stats.rb +26 -0
- data/lib/sec_edgar/derivative_transaction.rb +21 -0
- data/lib/sec_edgar/entity.rb +74 -0
- data/lib/sec_edgar/filing.rb +171 -0
- data/lib/sec_edgar/filing_parser.rb +216 -0
- data/lib/sec_edgar/filing_persister.rb +53 -0
- data/lib/sec_edgar/filing_updater.rb +52 -0
- data/lib/sec_edgar/footnote.rb +3 -0
- data/lib/sec_edgar/ftp_client.rb +35 -0
- data/lib/sec_edgar/nil_ownership_document.rb +39 -0
- data/lib/sec_edgar/officer_title.rb +39 -0
- data/lib/sec_edgar/ownership4Document.xsd.xml +141 -0
- data/lib/sec_edgar/ownershipDocumentCommon.xsd.xml +890 -0
- data/lib/sec_edgar/ownership_document.rb +121 -0
- data/lib/sec_edgar/poll.rb +132 -0
- data/lib/sec_edgar/sec_uri.rb +102 -0
- data/lib/sec_edgar/transaction.rb +20 -0
- data/lib/sec_edgar/utils.rb +28 -0
- data/lib/sec_edgar.rb +26 -0
- metadata +123 -0
@@ -0,0 +1,26 @@
|
|
1
|
+
module SecEdgar
|
2
|
+
module BasicStats
|
3
|
+
extend self
|
4
|
+
|
5
|
+
def precision(tp, fp)
|
6
|
+
tp.fdiv(tp + fp)
|
7
|
+
end
|
8
|
+
|
9
|
+
def recall(tp, fn)
|
10
|
+
tp.fdiv(tp + fn)
|
11
|
+
end
|
12
|
+
|
13
|
+
def f_beta(tp, fp, fn, beta)
|
14
|
+
p = precision(tp, fp)
|
15
|
+
r = recall(tp, fn)
|
16
|
+
b2 = beta ** 2
|
17
|
+
(1 + b2) * ((p * r).fdiv((b2 * p) + r))
|
18
|
+
end
|
19
|
+
|
20
|
+
def f1(tp, fp, fn)
|
21
|
+
p = precision(tp, fp)
|
22
|
+
r = recall(tp, fn)
|
23
|
+
2 * ((p * r).fdiv(p + r))
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module SecEdgar
|
2
|
+
DerivativeTransaction = Struct.new(
|
3
|
+
:acquired_or_disposed_code,
|
4
|
+
:conversion_or_exercise_price,
|
5
|
+
:deemed_execution_date,
|
6
|
+
:exercise_date,
|
7
|
+
:expiration_date,
|
8
|
+
:underlying_security_title,
|
9
|
+
:underlying_security_shares,
|
10
|
+
:nature_of_ownership,
|
11
|
+
:code,
|
12
|
+
:shares,
|
13
|
+
:security_title,
|
14
|
+
:direct_or_indirect_code,
|
15
|
+
:form_type,
|
16
|
+
:equity_swap_involved,
|
17
|
+
:transaction_date,
|
18
|
+
:shares_after,
|
19
|
+
:price_per_share
|
20
|
+
)
|
21
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
module SecEdgar
|
3
|
+
class Entity
|
4
|
+
COLUMNS = [
|
5
|
+
:cik,
|
6
|
+
:name,
|
7
|
+
:mailing_address,
|
8
|
+
:business_address,
|
9
|
+
:assigned_sic,
|
10
|
+
:assigned_sic_desc,
|
11
|
+
:assigned_sic_href,
|
12
|
+
:assitant_director,
|
13
|
+
:cik_href,
|
14
|
+
:formerly_names,
|
15
|
+
:state_location,
|
16
|
+
:state_location_href,
|
17
|
+
:state_of_incorporation
|
18
|
+
]
|
19
|
+
|
20
|
+
attr_accessor(*COLUMNS)
|
21
|
+
|
22
|
+
def initialize(entity)
|
23
|
+
COLUMNS.each do |column|
|
24
|
+
instance_variable_set("@#{ column }", entity[column.to_s])
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def filings
|
29
|
+
Filing.find(@cik)
|
30
|
+
end
|
31
|
+
|
32
|
+
def transactions
|
33
|
+
Transaction.find(@cik)
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.query(url)
|
37
|
+
RestClient.get(url) do |response, request, result, &block|
|
38
|
+
case response.code
|
39
|
+
when 200
|
40
|
+
return response
|
41
|
+
else
|
42
|
+
response.return!(request, result, &block)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# def self.find(entity_args)
|
48
|
+
# temp = {}
|
49
|
+
# temp[:url] = SecURI.browse_edgar_uri(entity_args)
|
50
|
+
# temp[:url][:action] = :getcompany
|
51
|
+
# response = query(temp[:url].output_atom.to_s)
|
52
|
+
# document = Nokogiri::HTML(response)
|
53
|
+
# xml = document.xpath("//feed/company-info")
|
54
|
+
# Entity.new(parse(xml))
|
55
|
+
# end
|
56
|
+
#
|
57
|
+
# def self.parse(xml)
|
58
|
+
# content = Hash.from_xml(xml.to_s)
|
59
|
+
# if content['company_info'].present?
|
60
|
+
# content = content['company_info']
|
61
|
+
# content['name'] = content.delete('conformed_name')
|
62
|
+
# if content['formerly_names'].present?
|
63
|
+
# content['formerly_names'] = content.delete('formerly_names')['names']
|
64
|
+
# end
|
65
|
+
# content['addresses']['address'].each do |address|
|
66
|
+
# content["#{address['type']}_address"] = address
|
67
|
+
# end
|
68
|
+
# return content
|
69
|
+
# else
|
70
|
+
# return {}
|
71
|
+
# end
|
72
|
+
# end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module SecEdgar
|
4
|
+
class Filing
|
5
|
+
COLUMNS = [:cik, :title, :summary, :link, :term, :date, :file_id]
|
6
|
+
|
7
|
+
attr_accessor(*COLUMNS)
|
8
|
+
|
9
|
+
def initialize(filing)
|
10
|
+
COLUMNS.each do |column|
|
11
|
+
instance_variable_set("@#{ column }", filing[column])
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.fetch(uri, &blk)
|
16
|
+
open(uri) do |rss|
|
17
|
+
parse_rss(rss, &blk)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.recent(options = {}, &blk)
|
22
|
+
start = options.fetch(:start, 0)
|
23
|
+
count = options.fetch(:count, 100)
|
24
|
+
limit = options.fetch(:limit, 100)
|
25
|
+
limited_count = [limit - start, count].min
|
26
|
+
fetch(uri_for_recent(start, limited_count), &blk)
|
27
|
+
start += count
|
28
|
+
return if start >= limit
|
29
|
+
recent({ start: start, count: count, limit: limit }, &blk)
|
30
|
+
rescue OpenURI::HTTPError => e
|
31
|
+
puts e
|
32
|
+
return
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.for_cik(cik, options = {}, &blk)
|
36
|
+
start = options.fetch(:start, 0)
|
37
|
+
count = options.fetch(:count, 100)
|
38
|
+
limit = options.fetch(:limit, 100)
|
39
|
+
fetch(uri_for_cik(cik, start, count), &blk)
|
40
|
+
start += count
|
41
|
+
return if start >= limit
|
42
|
+
for_cik(cik, { start: start, count: count, limit: limit }, &blk)
|
43
|
+
rescue OpenURI::HTTPError
|
44
|
+
return
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.for_date(date, &blk)
|
48
|
+
ftp = Net::FTP.new('ftp.sec.gov')
|
49
|
+
ftp.login
|
50
|
+
file_name = ftp.nlst("edgar/daily-index/#{ date.to_sec_uri_format }*")[0]
|
51
|
+
ftp.close
|
52
|
+
open("ftp://ftp.sec.gov/#{ file_name }") do |file|
|
53
|
+
if file_name[-2..-1] == 'gz'
|
54
|
+
gz_reader = Zlib::GzipReader.new(file)
|
55
|
+
gz_reader.rewind
|
56
|
+
filings_for_index(gz_reader).each(&blk)
|
57
|
+
else
|
58
|
+
filings_for_index(file).each(&blk)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
rescue Net::FTPTempError
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.filings_for_index(index)
|
65
|
+
[].tap do |filings|
|
66
|
+
content_section = false
|
67
|
+
index.each_line do |row|
|
68
|
+
content_section = true if row.include?('-------------')
|
69
|
+
next if !content_section || row.include?('------------')
|
70
|
+
filing = filing_for_index_row(row)
|
71
|
+
filings << filing unless filing.nil?
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.filing_for_index_row(row)
|
77
|
+
data = row.split(/ /).reject(&:blank?).map(&:strip)
|
78
|
+
data = row.split(/ /).reject(&:blank?).map(&:strip) if data.count == 4
|
79
|
+
data[1].gsub!('/ADV', '')
|
80
|
+
data.delete_at(1) if data[1][0] == '/'
|
81
|
+
return nil unless Regexp.new(/\d{8}/).match(data[3])
|
82
|
+
unless data[4][0..3] == 'http'
|
83
|
+
data[4] = "http://www.sec.gov/Archives/#{ data[4] }"
|
84
|
+
end
|
85
|
+
Filing.new(
|
86
|
+
term: data[1],
|
87
|
+
cik: data[2],
|
88
|
+
date: Date.parse(data[3]),
|
89
|
+
link: data[4]
|
90
|
+
)
|
91
|
+
end
|
92
|
+
|
93
|
+
def self.uri_for_recent(start = 0, count = 100)
|
94
|
+
SecURI.browse_edgar_uri(
|
95
|
+
action: :getcurrent,
|
96
|
+
owner: :include,
|
97
|
+
output: :atom,
|
98
|
+
start: start,
|
99
|
+
count: count
|
100
|
+
)
|
101
|
+
end
|
102
|
+
|
103
|
+
def self.uri_for_cik(cik, start = 0, count = 100)
|
104
|
+
SecURI.browse_edgar_uri(
|
105
|
+
action: :getcompany,
|
106
|
+
owner: :include,
|
107
|
+
output: :atom,
|
108
|
+
start: start,
|
109
|
+
count: count,
|
110
|
+
CIK: cik
|
111
|
+
)
|
112
|
+
end
|
113
|
+
|
114
|
+
def self.parse_rss(rss, &blk)
|
115
|
+
feed = RSS::Parser.parse(rss, false)
|
116
|
+
feed.entries.each do |entry|
|
117
|
+
filing = Filing.new(
|
118
|
+
cik: entry.title.content.match(/\((\w{10})\)/)[1],
|
119
|
+
file_id: entry.id.content.split('=').last,
|
120
|
+
term: entry.category.term,
|
121
|
+
title: entry.title.content,
|
122
|
+
summary: entry.summary.content,
|
123
|
+
date: DateTime.parse(entry.updated.content.to_s),
|
124
|
+
link: entry.link.href.gsub('-index.htm', '.txt')
|
125
|
+
)
|
126
|
+
blk.call(filing)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# def self.find(cik, start = 0, count = 80)
|
131
|
+
# temp = {}
|
132
|
+
# temp[:url] = SecURI.browse_edgar_uri({cik: cik})
|
133
|
+
# temp[:url][:action] = :getcompany
|
134
|
+
# temp[:url][:start] = start
|
135
|
+
# temp[:url][:count] = count
|
136
|
+
# response = Entity.query(temp[:url].output_atom.to_s)
|
137
|
+
# document = Nokogiri::HTML(response)
|
138
|
+
# parse(cik, document)
|
139
|
+
# end
|
140
|
+
|
141
|
+
def self.parse(cik, document)
|
142
|
+
filings = []
|
143
|
+
if document.xpath('//content').to_s.length > 0
|
144
|
+
document.xpath('//content').each do |e|
|
145
|
+
if e.xpath('//content/accession-nunber').to_s.length > 0
|
146
|
+
content = Hash.from_xml(e.to_s)['content']
|
147
|
+
content[:cik] = cik
|
148
|
+
content[:file_id] = content.delete('accession_nunber')
|
149
|
+
content[:date] = content.delete('filing_date')
|
150
|
+
content[:link] = content.delete('filing_href')
|
151
|
+
content[:term] = content.delete('filing_type')
|
152
|
+
content[:title] = content.delete('form_name')
|
153
|
+
filings << Filing.new(content)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
filings
|
158
|
+
end
|
159
|
+
|
160
|
+
def content(&error_blk)
|
161
|
+
@content ||= RestClient.get(self.link)
|
162
|
+
rescue RestClient::ResourceNotFound => e
|
163
|
+
puts "404 Resource Not Found: Bad link #{ self.link }"
|
164
|
+
if block_given?
|
165
|
+
error_blk.call(e, self)
|
166
|
+
else
|
167
|
+
raise e
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,216 @@
|
|
1
|
+
module SecEdgar
|
2
|
+
class FilingParser
|
3
|
+
attr_reader :xsd_uri
|
4
|
+
|
5
|
+
def initialize(filing)
|
6
|
+
@xsd_uri = 'lib/sec4/ownership4Document.xsd.xml'
|
7
|
+
@filing = filing
|
8
|
+
@content = filing.content
|
9
|
+
end
|
10
|
+
|
11
|
+
def content
|
12
|
+
if @content.start_with?('-----BEGIN PRIVACY-ENHANCED MESSAGE-----')
|
13
|
+
@content = strip_privacy_wrapper(@content)
|
14
|
+
end
|
15
|
+
@content
|
16
|
+
end
|
17
|
+
|
18
|
+
def doc
|
19
|
+
@doc ||= OwnershipDocument.new
|
20
|
+
end
|
21
|
+
|
22
|
+
def parse(&error_blk)
|
23
|
+
if block_given? && !xml_valid?
|
24
|
+
error_blk.call(xml_errors)
|
25
|
+
puts "Error: returning NilObjectDocument #{ @filing.link }"
|
26
|
+
return NilOwnershipDocument.new
|
27
|
+
end
|
28
|
+
|
29
|
+
footnotes | transactions | derivative_transactions # eager init
|
30
|
+
parse_doc(xml_doc)
|
31
|
+
parse_issuer(xml_doc.xpath('//issuer'))
|
32
|
+
parse_owner(xml_doc.xpath('//reportingOwner'))
|
33
|
+
parse_non_derivative_table(xml_doc.xpath('//nonDerivativeTable'))
|
34
|
+
parse_derivative_table(xml_doc.xpath('//derivativeTable'))
|
35
|
+
parse_footnotes(xml_doc.xpath('//footnotes'))
|
36
|
+
doc
|
37
|
+
end
|
38
|
+
|
39
|
+
def footnotes
|
40
|
+
doc.footnotes ||= []
|
41
|
+
end
|
42
|
+
|
43
|
+
def transactions
|
44
|
+
doc.transactions ||= []
|
45
|
+
end
|
46
|
+
|
47
|
+
def derivative_transactions
|
48
|
+
doc.derivative_transactions ||= []
|
49
|
+
end
|
50
|
+
|
51
|
+
def xml_filing
|
52
|
+
Nokogiri::XML(xml_doc.to_xml)
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def strip_privacy_wrapper(raw)
|
58
|
+
lines = raw.split("\n")
|
59
|
+
lines.shift until lines.first.start_with?('<SEC-DOCUMENT>')
|
60
|
+
lines.pop if lines.last.start_with?('-----END PRIVACY-ENHANCED MESSAGE--')
|
61
|
+
lines.join("\n")
|
62
|
+
end
|
63
|
+
|
64
|
+
def parse_transaction(el)
|
65
|
+
transaction = Transaction.new
|
66
|
+
transaction.security_title = el.xpath('securityTitle').text.strip
|
67
|
+
transaction.transaction_date = Date.parse(el.xpath('transactionDate').text)
|
68
|
+
|
69
|
+
parse_transaction_amounts(transaction, el.xpath('transactionAmounts'))
|
70
|
+
parse_transaction_ownership_nature(transaction, el.xpath('ownershipNature'))
|
71
|
+
parse_transaction_coding(transaction, el.xpath('transactionCoding'))
|
72
|
+
|
73
|
+
# post transaction amounts
|
74
|
+
transaction.shares_after = el
|
75
|
+
.xpath('postTransactionAmounts/sharesOwnedFollowingTransaction/value')
|
76
|
+
.text
|
77
|
+
.to_f
|
78
|
+
transactions << transaction
|
79
|
+
end
|
80
|
+
|
81
|
+
def parse_footnote(el)
|
82
|
+
footnote = Footnote.new
|
83
|
+
footnote.content = el.text.strip
|
84
|
+
footnote.id = el.attribute("id").value
|
85
|
+
footnotes << footnote
|
86
|
+
end
|
87
|
+
|
88
|
+
def parse_derivative_transaction(el)
|
89
|
+
transaction = DerivativeTransaction.new
|
90
|
+
|
91
|
+
transaction.security_title = el.xpath('securityTitle').text.strip
|
92
|
+
transaction.transaction_date = Date.parse(el.xpath('transactionDate').text)
|
93
|
+
transaction.conversion_or_exercise_price = el.xpath('conversionOrExercisePrice').text.to_f
|
94
|
+
|
95
|
+
unless (expiration_date = el.xpath('expirationDate/value').text).blank?
|
96
|
+
transaction.expiration_date = Date.parse(expiration_date)
|
97
|
+
end
|
98
|
+
|
99
|
+
unless (exercise_date = el.xpath('exerciseDate/value').text).blank?
|
100
|
+
transaction.exercise_date = Date.parse(exercise_date)
|
101
|
+
end
|
102
|
+
|
103
|
+
parse_transaction_amounts(transaction, el.xpath('transactionAmounts'))
|
104
|
+
parse_transaction_ownership_nature(transaction, el.xpath('ownershipNature'))
|
105
|
+
parse_transaction_coding(transaction, el.xpath('transactionCoding'))
|
106
|
+
parse_transaction_underlying(transaction, el.xpath('underlyingSecurity'))
|
107
|
+
|
108
|
+
# post transaction amounts
|
109
|
+
transaction.shares_after = el
|
110
|
+
.xpath('postTransactionAmounts/sharesOwnedFollowingTransaction/value')
|
111
|
+
.text
|
112
|
+
.to_f
|
113
|
+
derivative_transactions << transaction
|
114
|
+
end
|
115
|
+
|
116
|
+
def parse_transaction_underlying(transaction, el)
|
117
|
+
transaction.underlying_security_title = el.xpath('underlyingSecurityTitle/value').text
|
118
|
+
transaction.underlying_security_shares =
|
119
|
+
el.xpath('underlyingSecurityShares/value').text.to_f
|
120
|
+
end
|
121
|
+
|
122
|
+
def parse_non_derivative_table(el)
|
123
|
+
el.xpath('//nonDerivativeTransaction').each do |transaction_el|
|
124
|
+
parse_transaction(transaction_el)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def parse_derivative_table(el)
|
129
|
+
el.xpath('//derivativeTransaction').each do |transaction_el|
|
130
|
+
parse_derivative_transaction(transaction_el)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def parse_footnotes(el)
|
135
|
+
el.xpath('//footnote').each do |note_el|
|
136
|
+
parse_footnote(note_el)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def parse_transaction_amounts(transaction, el)
|
141
|
+
transaction.acquired_or_disposed_code =
|
142
|
+
el.xpath('transactionAcquiredDisposedCode/value').text
|
143
|
+
transaction.shares =
|
144
|
+
el.xpath('transactionShares/value').text.to_f
|
145
|
+
transaction.price_per_share =
|
146
|
+
el.xpath('transactionPricePerShare/value').text.to_f
|
147
|
+
end
|
148
|
+
|
149
|
+
def parse_transaction_coding(transaction, el)
|
150
|
+
transaction.code = el.xpath('transactionCode').text
|
151
|
+
transaction.form_type = el.xpath('transactionFormType').text
|
152
|
+
transaction.equity_swap_involved = el.xpath('equitySwapInvolved').text == '1'
|
153
|
+
end
|
154
|
+
|
155
|
+
def parse_transaction_ownership_nature(transaction, el)
|
156
|
+
transaction.nature_of_ownership = el.xpath('natureOfOwnership/value').text
|
157
|
+
transaction.direct_or_indirect_code = el.xpath('directOrIndirectOwnership/value').text
|
158
|
+
end
|
159
|
+
|
160
|
+
def parse_doc(el)
|
161
|
+
doc.schema_version = el.xpath('//schemaVersion').text
|
162
|
+
doc.document_type = el.xpath('//documentType').text
|
163
|
+
doc.not_subject_to_section_16 =
|
164
|
+
(el.xpath('//notSubjectToSection16').text == '1' ||
|
165
|
+
el.xpath('//notSubjectToSection16').text == 'true')
|
166
|
+
doc.period_of_report = Date.parse(el.xpath('//periodOfReport').text)
|
167
|
+
rescue ArgumentError => e
|
168
|
+
puts "parse_doc error: #{ el.inspect }"
|
169
|
+
puts e
|
170
|
+
raise e
|
171
|
+
end
|
172
|
+
|
173
|
+
def parse_issuer(el)
|
174
|
+
doc.issuer_cik = el.xpath('//issuerCik').text
|
175
|
+
doc.issuer_name = el.xpath('//issuerName').text
|
176
|
+
doc.issuer_trading_symbol = el.xpath('//issuerTradingSymbol').text
|
177
|
+
end
|
178
|
+
|
179
|
+
def parse_owner(el)
|
180
|
+
doc.owner_cik = el.xpath('//rptOwnerCik').text
|
181
|
+
doc.owner_name = el.xpath('//rptOwnerName').text
|
182
|
+
doc.is_director = el.xpath('//isDirector').text == '1'
|
183
|
+
doc.is_ten_percent_owner = el.xpath('//isTenPercentOwner').text == '1'
|
184
|
+
doc.is_other = el.xpath('//isOther').text == '1'
|
185
|
+
doc.is_officer =
|
186
|
+
(el.xpath('//isOfficer').text == '1' ||
|
187
|
+
el.xpath('//isOfficer').text.downcase == 'true')
|
188
|
+
doc.officer_title = el.xpath('//officerTitle').text
|
189
|
+
|
190
|
+
address = Address.new
|
191
|
+
address.street1 = el.xpath('//rptOwnerStreet1').text
|
192
|
+
address.street2 = el.xpath('//rptOwnerStreet2').text
|
193
|
+
address.city = el.xpath('//rptOwnerCity').text
|
194
|
+
address.state = el.xpath('//rptOwnerState').text
|
195
|
+
address.zip = el.xpath('//rptOwnerZipCode').text
|
196
|
+
address.state_description = el.xpath('//rptOwnerStateDescription').text
|
197
|
+
doc.owner_address = address
|
198
|
+
end
|
199
|
+
|
200
|
+
def xml_doc
|
201
|
+
Nokogiri::XML(content).xpath('//ownershipDocument')
|
202
|
+
end
|
203
|
+
|
204
|
+
def xml_schema
|
205
|
+
@schema ||= Nokogiri::XML::Schema(IO.read(xsd_uri))
|
206
|
+
end
|
207
|
+
|
208
|
+
def xml_valid?
|
209
|
+
xml_errors.empty? && !content.blank?
|
210
|
+
end
|
211
|
+
|
212
|
+
def xml_errors
|
213
|
+
@errors ||= xml_schema.validate(xml_filing)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# module SecEdgar
|
2
|
+
# class FilingPersister
|
3
|
+
# attr_reader :filing
|
4
|
+
#
|
5
|
+
# def initialize(filing)
|
6
|
+
# @filing = filing
|
7
|
+
# end
|
8
|
+
#
|
9
|
+
# def doc
|
10
|
+
# @doc ||= RawFiling.for_filing(filing).parsed
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# def persist!
|
14
|
+
# return form if form = Form4.find_by(link: filing.link)
|
15
|
+
# doc_json = doc.to_json
|
16
|
+
# return if doc_json == '{}'
|
17
|
+
#
|
18
|
+
# form = Form4.new(
|
19
|
+
# cik: filing.cik,
|
20
|
+
# title: filing.title,
|
21
|
+
# link: filing.link,
|
22
|
+
# term: filing.term,
|
23
|
+
# date: filing.date,
|
24
|
+
# file_id: filing.file_id,
|
25
|
+
# dollar_volume: doc.dollar_volume,
|
26
|
+
# document: { d: doc_json }
|
27
|
+
# )
|
28
|
+
# form.company = Company.where(cik: doc.issuer_cik).first_or_initialize
|
29
|
+
# form.company.update_attributes(
|
30
|
+
# name: doc.issuer_name,
|
31
|
+
# ticker: doc.issuer_trading_symbol.upcase
|
32
|
+
# )
|
33
|
+
#
|
34
|
+
# form.insider = Insider.where(cik: doc.owner_cik).first_or_initialize
|
35
|
+
# form.insider.update_attributes(
|
36
|
+
# name: doc.owner_name[0, 254]
|
37
|
+
# )
|
38
|
+
#
|
39
|
+
# form.day_traded_price = form.company.price_on(form.date)
|
40
|
+
# form.day_traded_volume = form.company.volume_on(form.date)
|
41
|
+
# form.plus_3_months_price = form.company.price_on(form.date + 3.months)
|
42
|
+
# form.plus_6_months_price = form.company.price_on(form.date + 6.months)
|
43
|
+
# form.plus_12_months_price = form.company.price_on(form.date + 12.months)
|
44
|
+
# # need to find a more detailed data source
|
45
|
+
# # form.price_to_earnings = Company.price_to_earnings_on(form.date)
|
46
|
+
# # form.price_to_book = Company.price_to_book_on(form.date)
|
47
|
+
#
|
48
|
+
# form.doc = doc
|
49
|
+
# form.save!
|
50
|
+
# form
|
51
|
+
# end
|
52
|
+
# end
|
53
|
+
# end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module SecEdgar
|
2
|
+
class FilingUpdater
|
3
|
+
attr_reader :form
|
4
|
+
|
5
|
+
def initialize(form)
|
6
|
+
@form = form
|
7
|
+
end
|
8
|
+
|
9
|
+
def doc
|
10
|
+
@doc ||= RawFiling.for_form(form).parsed
|
11
|
+
end
|
12
|
+
|
13
|
+
def update
|
14
|
+
doc_json = doc.to_json
|
15
|
+
return if doc_json == '{}'
|
16
|
+
|
17
|
+
form.update({
|
18
|
+
dollar_volume: doc.dollar_volume,
|
19
|
+
document: { d: doc_json }
|
20
|
+
})
|
21
|
+
|
22
|
+
unless form.company
|
23
|
+
form.company = Company.where(cik: doc.issuer_cik).first_or_initialize
|
24
|
+
form.company.update_attributes(
|
25
|
+
name: doc.issuer_name,
|
26
|
+
ticker: doc.issuer_trading_symbol.upcase
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
unless form.insider
|
31
|
+
form.insider = Insider.where(cik: doc.owner_cik).first_or_initialize
|
32
|
+
form.insider.update_attributes(
|
33
|
+
name: doc.owner_name[0, 254]
|
34
|
+
)
|
35
|
+
end
|
36
|
+
|
37
|
+
dt = Date.parse(form.date)
|
38
|
+
form.day_traded_price = form.company.price_on(dt)
|
39
|
+
form.day_traded_volume = form.company.volume_on(dt)
|
40
|
+
form.plus_3_months_price = form.company.price_on(dt + 3.months)
|
41
|
+
form.plus_6_months_price = form.company.price_on(dt + 6.months)
|
42
|
+
form.plus_12_months_price = form.company.price_on(dt + 12.months)
|
43
|
+
# need to find a more detailed data source
|
44
|
+
# form.price_to_earnings = Company.price_to_earnings_on(form.date)
|
45
|
+
# form.price_to_book = Company.price_to_book_on(form.date)
|
46
|
+
|
47
|
+
form.doc = doc
|
48
|
+
form.save!
|
49
|
+
form
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module SecEdgar
|
2
|
+
class FtpClient
|
3
|
+
include Singleton
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@ftp = Net::FTP.new
|
7
|
+
@ftp.passive = true
|
8
|
+
connect
|
9
|
+
login
|
10
|
+
|
11
|
+
at_exit do
|
12
|
+
puts "Closing Sec4::FtpClient..."
|
13
|
+
@ftp.close
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def fetch(remote_url, local_url)
|
18
|
+
@ftp.getbinaryfile(remote_url, local_url)
|
19
|
+
end
|
20
|
+
|
21
|
+
def connect
|
22
|
+
@ftp.connect('ftp.sec.gov', 21)
|
23
|
+
rescue => e
|
24
|
+
puts "Sec4::FtpClient connection failed"
|
25
|
+
puts e.message
|
26
|
+
end
|
27
|
+
|
28
|
+
def login
|
29
|
+
@ftp.login
|
30
|
+
rescue => e
|
31
|
+
puts "Sec4::FtpClient login failed"
|
32
|
+
puts e.message
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Sec4
|
2
|
+
class NilOwnershipDocument
|
3
|
+
def document_type
|
4
|
+
'4'
|
5
|
+
end
|
6
|
+
|
7
|
+
def issuer_trading_symbol
|
8
|
+
'NULL'
|
9
|
+
end
|
10
|
+
|
11
|
+
def is_director
|
12
|
+
false
|
13
|
+
end
|
14
|
+
|
15
|
+
def is_officer
|
16
|
+
false
|
17
|
+
end
|
18
|
+
|
19
|
+
def is_owner
|
20
|
+
false
|
21
|
+
end
|
22
|
+
|
23
|
+
def is_ten_percent_owner
|
24
|
+
false
|
25
|
+
end
|
26
|
+
|
27
|
+
def officer_title
|
28
|
+
''
|
29
|
+
end
|
30
|
+
|
31
|
+
def transactions
|
32
|
+
[]
|
33
|
+
end
|
34
|
+
|
35
|
+
def derivative_transactions
|
36
|
+
[]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|