sec_edgar 0.0.1
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 +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
|