sec_api 1.0.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 +7 -0
- data/.devcontainer/Dockerfile +54 -0
- data/.devcontainer/README.md +178 -0
- data/.devcontainer/devcontainer.json +46 -0
- data/.devcontainer/docker-compose.yml +28 -0
- data/.devcontainer/post-create.sh +51 -0
- data/.devcontainer/post-start.sh +44 -0
- data/.rspec +3 -0
- data/.standard.yml +3 -0
- data/CHANGELOG.md +5 -0
- data/CLAUDE.md +0 -0
- data/LICENSE.txt +21 -0
- data/MIGRATION.md +274 -0
- data/README.md +370 -0
- data/Rakefile +10 -0
- data/config/secapi.yml.example +57 -0
- data/docs/development-guide.md +291 -0
- data/docs/enumerator_pattern_design.md +483 -0
- data/docs/examples/README.md +58 -0
- data/docs/examples/backfill_filings.rb +419 -0
- data/docs/examples/instrumentation.rb +583 -0
- data/docs/examples/query_builder.rb +308 -0
- data/docs/examples/streaming_notifications.rb +491 -0
- data/docs/index.md +244 -0
- data/docs/migration-guide-v1.md +1091 -0
- data/docs/pre-review-checklist.md +145 -0
- data/docs/project-overview.md +90 -0
- data/docs/project-scan-report.json +60 -0
- data/docs/source-tree-analysis.md +190 -0
- data/lib/sec_api/callback_helper.rb +49 -0
- data/lib/sec_api/client.rb +606 -0
- data/lib/sec_api/collections/filings.rb +267 -0
- data/lib/sec_api/collections/fulltext_results.rb +86 -0
- data/lib/sec_api/config.rb +590 -0
- data/lib/sec_api/deep_freezable.rb +42 -0
- data/lib/sec_api/errors/authentication_error.rb +24 -0
- data/lib/sec_api/errors/configuration_error.rb +5 -0
- data/lib/sec_api/errors/error.rb +75 -0
- data/lib/sec_api/errors/network_error.rb +26 -0
- data/lib/sec_api/errors/not_found_error.rb +23 -0
- data/lib/sec_api/errors/pagination_error.rb +28 -0
- data/lib/sec_api/errors/permanent_error.rb +29 -0
- data/lib/sec_api/errors/rate_limit_error.rb +57 -0
- data/lib/sec_api/errors/reconnection_error.rb +34 -0
- data/lib/sec_api/errors/server_error.rb +25 -0
- data/lib/sec_api/errors/transient_error.rb +28 -0
- data/lib/sec_api/errors/validation_error.rb +23 -0
- data/lib/sec_api/extractor.rb +122 -0
- data/lib/sec_api/filing_journey.rb +477 -0
- data/lib/sec_api/mapping.rb +125 -0
- data/lib/sec_api/metrics_collector.rb +411 -0
- data/lib/sec_api/middleware/error_handler.rb +250 -0
- data/lib/sec_api/middleware/instrumentation.rb +186 -0
- data/lib/sec_api/middleware/rate_limiter.rb +541 -0
- data/lib/sec_api/objects/data_file.rb +34 -0
- data/lib/sec_api/objects/document_format_file.rb +45 -0
- data/lib/sec_api/objects/entity.rb +92 -0
- data/lib/sec_api/objects/extracted_data.rb +118 -0
- data/lib/sec_api/objects/fact.rb +147 -0
- data/lib/sec_api/objects/filing.rb +197 -0
- data/lib/sec_api/objects/fulltext_result.rb +66 -0
- data/lib/sec_api/objects/period.rb +96 -0
- data/lib/sec_api/objects/stream_filing.rb +194 -0
- data/lib/sec_api/objects/xbrl_data.rb +356 -0
- data/lib/sec_api/query.rb +423 -0
- data/lib/sec_api/rate_limit_state.rb +130 -0
- data/lib/sec_api/rate_limit_tracker.rb +154 -0
- data/lib/sec_api/stream.rb +841 -0
- data/lib/sec_api/structured_logger.rb +199 -0
- data/lib/sec_api/types.rb +32 -0
- data/lib/sec_api/version.rb +42 -0
- data/lib/sec_api/xbrl.rb +220 -0
- data/lib/sec_api.rb +137 -0
- data/sig/sec_api.rbs +4 -0
- metadata +217 -0
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Example: Real-Time Streaming Notifications
|
|
5
|
+
#
|
|
6
|
+
# This example demonstrates:
|
|
7
|
+
# - WebSocket connection with basic subscription
|
|
8
|
+
# - Ticker and form type filtering
|
|
9
|
+
# - Callback handler with proper error isolation
|
|
10
|
+
# - Auto-reconnect behavior and monitoring
|
|
11
|
+
# - Sidekiq integration for async processing
|
|
12
|
+
# - Latency monitoring and alerting
|
|
13
|
+
# - Graceful shutdown pattern
|
|
14
|
+
#
|
|
15
|
+
# Prerequisites:
|
|
16
|
+
# - gem install sec_api
|
|
17
|
+
# - Set SECAPI_API_KEY environment variable
|
|
18
|
+
#
|
|
19
|
+
# Usage:
|
|
20
|
+
# ruby docs/examples/streaming_notifications.rb
|
|
21
|
+
#
|
|
22
|
+
# Note: This example demonstrates the streaming API patterns.
|
|
23
|
+
# Running it will connect to the SEC API WebSocket stream.
|
|
24
|
+
# Press Ctrl+C to stop the stream.
|
|
25
|
+
|
|
26
|
+
require "sec_api"
|
|
27
|
+
|
|
28
|
+
# =============================================================================
|
|
29
|
+
# SECTION 1: Basic WebSocket Subscription
|
|
30
|
+
# =============================================================================
|
|
31
|
+
|
|
32
|
+
puts "=" * 60
|
|
33
|
+
puts "SECTION 1: Basic WebSocket Subscription"
|
|
34
|
+
puts "=" * 60
|
|
35
|
+
|
|
36
|
+
# NOTE: These examples demonstrate the patterns.
|
|
37
|
+
# Uncomment the subscription calls to actually connect.
|
|
38
|
+
|
|
39
|
+
# Basic subscription - receives ALL filings
|
|
40
|
+
puts "\nPattern: Basic subscription (all filings)"
|
|
41
|
+
puts <<~CODE
|
|
42
|
+
client = SecApi::Client.new
|
|
43
|
+
client.stream.subscribe do |filing|
|
|
44
|
+
puts "\#{filing.ticker}: \#{filing.form_type} filed at \#{filing.filed_at}"
|
|
45
|
+
end
|
|
46
|
+
CODE
|
|
47
|
+
|
|
48
|
+
# =============================================================================
|
|
49
|
+
# SECTION 2: Ticker and Form Type Filtering
|
|
50
|
+
# =============================================================================
|
|
51
|
+
|
|
52
|
+
puts "\n" + "=" * 60
|
|
53
|
+
puts "SECTION 2: Ticker and Form Type Filtering"
|
|
54
|
+
puts "=" * 60
|
|
55
|
+
|
|
56
|
+
# Filter by ticker(s) - client-side filtering
|
|
57
|
+
puts "\nPattern: Filter by tickers"
|
|
58
|
+
puts <<~CODE
|
|
59
|
+
client.stream.subscribe(tickers: ["AAPL", "TSLA", "GOOGL"]) do |filing|
|
|
60
|
+
puts "Watched company: \#{filing.ticker} - \#{filing.form_type}"
|
|
61
|
+
end
|
|
62
|
+
CODE
|
|
63
|
+
|
|
64
|
+
# Filter by form type(s) - client-side filtering
|
|
65
|
+
puts "\nPattern: Filter by form types"
|
|
66
|
+
puts <<~CODE
|
|
67
|
+
# Only material events and annual/quarterly reports
|
|
68
|
+
client.stream.subscribe(form_types: ["10-K", "10-Q", "8-K"]) do |filing|
|
|
69
|
+
puts "Material filing: \#{filing.form_type}"
|
|
70
|
+
end
|
|
71
|
+
CODE
|
|
72
|
+
|
|
73
|
+
# Combined filters (AND logic)
|
|
74
|
+
puts "\nPattern: Combined filters (ticker AND form_type)"
|
|
75
|
+
puts <<~CODE
|
|
76
|
+
# Only 10-K and 10-Q for specific tickers
|
|
77
|
+
client.stream.subscribe(
|
|
78
|
+
tickers: ["AAPL", "MSFT"],
|
|
79
|
+
form_types: ["10-K", "10-Q"]
|
|
80
|
+
) do |filing|
|
|
81
|
+
puts "Quarterly/Annual report for \#{filing.ticker}"
|
|
82
|
+
analyze_financials(filing)
|
|
83
|
+
end
|
|
84
|
+
CODE
|
|
85
|
+
|
|
86
|
+
# Amendments are matched automatically
|
|
87
|
+
puts "\nPattern: Amendments matching"
|
|
88
|
+
puts <<~CODE
|
|
89
|
+
# Filter "10-K" also matches "10-K/A" (amendments)
|
|
90
|
+
client.stream.subscribe(form_types: ["10-K"]) do |filing|
|
|
91
|
+
# This receives both 10-K and 10-K/A filings
|
|
92
|
+
puts "\#{filing.form_type}" # Could be "10-K" or "10-K/A"
|
|
93
|
+
end
|
|
94
|
+
CODE
|
|
95
|
+
|
|
96
|
+
# =============================================================================
|
|
97
|
+
# SECTION 3: Callback Handler with Error Isolation
|
|
98
|
+
# =============================================================================
|
|
99
|
+
|
|
100
|
+
puts "\n" + "=" * 60
|
|
101
|
+
puts "SECTION 3: Error Isolation in Callbacks"
|
|
102
|
+
puts "=" * 60
|
|
103
|
+
|
|
104
|
+
# Errors in your callback don't crash the stream
|
|
105
|
+
puts "\nPattern: Error-safe callback processing"
|
|
106
|
+
puts <<~CODE
|
|
107
|
+
client = SecApi::Client.new(
|
|
108
|
+
api_key: ENV.fetch("SECAPI_API_KEY"),
|
|
109
|
+
on_callback_error: ->(info) {
|
|
110
|
+
# Called when your callback raises an exception
|
|
111
|
+
Bugsnag.notify(info[:error], {
|
|
112
|
+
accession_no: info[:accession_no],
|
|
113
|
+
ticker: info[:ticker]
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
client.stream.subscribe do |filing|
|
|
119
|
+
# If this raises, on_callback_error is called and stream continues
|
|
120
|
+
process_filing(filing) # May raise!
|
|
121
|
+
end
|
|
122
|
+
CODE
|
|
123
|
+
|
|
124
|
+
# Defensive callback pattern
|
|
125
|
+
puts "\nPattern: Defensive callback with rescue"
|
|
126
|
+
puts <<~CODE
|
|
127
|
+
client.stream.subscribe do |filing|
|
|
128
|
+
begin
|
|
129
|
+
# Your processing logic
|
|
130
|
+
validate_filing(filing)
|
|
131
|
+
store_filing(filing)
|
|
132
|
+
trigger_alerts(filing)
|
|
133
|
+
rescue StandardError => e
|
|
134
|
+
# Log error but don't re-raise - stream continues
|
|
135
|
+
Rails.logger.error("Filing callback failed: \#{e.message}")
|
|
136
|
+
ErrorTracker.capture(e, filing: filing.to_h)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
CODE
|
|
140
|
+
|
|
141
|
+
# =============================================================================
|
|
142
|
+
# SECTION 4: Auto-Reconnect Behavior and Monitoring
|
|
143
|
+
# =============================================================================
|
|
144
|
+
|
|
145
|
+
puts "\n" + "=" * 60
|
|
146
|
+
puts "SECTION 4: Auto-Reconnect and Monitoring"
|
|
147
|
+
puts "=" * 60
|
|
148
|
+
|
|
149
|
+
# Configure reconnection behavior
|
|
150
|
+
puts "\nPattern: Custom reconnection settings"
|
|
151
|
+
puts <<~CODE
|
|
152
|
+
config = SecApi::Config.new(
|
|
153
|
+
api_key: ENV.fetch("SECAPI_API_KEY"),
|
|
154
|
+
|
|
155
|
+
# Reconnection settings
|
|
156
|
+
stream_max_reconnect_attempts: 10, # Give up after 10 attempts
|
|
157
|
+
stream_initial_reconnect_delay: 1.0, # Start with 1 second
|
|
158
|
+
stream_max_reconnect_delay: 60.0, # Cap at 1 minute
|
|
159
|
+
stream_backoff_multiplier: 2, # Exponential: 1s, 2s, 4s, 8s...
|
|
160
|
+
|
|
161
|
+
# Called on successful reconnection
|
|
162
|
+
on_reconnect: ->(info) {
|
|
163
|
+
Rails.logger.info("Stream reconnected", {
|
|
164
|
+
attempts: info[:attempt_count],
|
|
165
|
+
downtime_seconds: info[:downtime_seconds]
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
# Alert if downtime was significant
|
|
169
|
+
if info[:downtime_seconds] > 60
|
|
170
|
+
AlertService.warn("SEC stream reconnected after \#{info[:downtime_seconds]}s downtime")
|
|
171
|
+
end
|
|
172
|
+
}
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
client = SecApi::Client.new(config)
|
|
176
|
+
CODE
|
|
177
|
+
|
|
178
|
+
# Backfill after reconnection
|
|
179
|
+
puts "\nPattern: Backfill missed filings after reconnection"
|
|
180
|
+
puts <<~CODE
|
|
181
|
+
last_filing_time = nil
|
|
182
|
+
reconnected = false
|
|
183
|
+
|
|
184
|
+
config = SecApi::Config.new(
|
|
185
|
+
api_key: ENV.fetch("SECAPI_API_KEY"),
|
|
186
|
+
on_reconnect: ->(info) {
|
|
187
|
+
reconnected = true
|
|
188
|
+
}
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
client = SecApi::Client.new(config)
|
|
192
|
+
|
|
193
|
+
client.stream.subscribe(tickers: ["AAPL"]) do |filing|
|
|
194
|
+
if reconnected && last_filing_time
|
|
195
|
+
# Backfill any missed filings via Query API
|
|
196
|
+
missed = client.query
|
|
197
|
+
.ticker("AAPL")
|
|
198
|
+
.date_range(from: last_filing_time, to: Time.now)
|
|
199
|
+
.search
|
|
200
|
+
|
|
201
|
+
missed.each { |f| process_filing(f) }
|
|
202
|
+
reconnected = false
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
last_filing_time = filing.filed_at
|
|
206
|
+
process_filing(filing)
|
|
207
|
+
end
|
|
208
|
+
CODE
|
|
209
|
+
|
|
210
|
+
# =============================================================================
|
|
211
|
+
# SECTION 5: Sidekiq/Background Job Integration
|
|
212
|
+
# =============================================================================
|
|
213
|
+
|
|
214
|
+
puts "\n" + "=" * 60
|
|
215
|
+
puts "SECTION 5: Background Job Integration"
|
|
216
|
+
puts "=" * 60
|
|
217
|
+
|
|
218
|
+
# Sidekiq integration - keep callbacks fast
|
|
219
|
+
puts "\nPattern: Sidekiq job enqueueing"
|
|
220
|
+
puts <<~CODE
|
|
221
|
+
# Job class (in app/workers/process_filing_worker.rb)
|
|
222
|
+
class ProcessFilingWorker
|
|
223
|
+
include Sidekiq::Worker
|
|
224
|
+
|
|
225
|
+
def perform(accession_no, ticker, form_type, filed_at)
|
|
226
|
+
# Fetch full filing details if needed
|
|
227
|
+
client = SecApi::Client.new
|
|
228
|
+
filings = client.query
|
|
229
|
+
.ticker(ticker)
|
|
230
|
+
.form_type(form_type)
|
|
231
|
+
.limit(1)
|
|
232
|
+
.search
|
|
233
|
+
|
|
234
|
+
filing = filings.first
|
|
235
|
+
return unless filing
|
|
236
|
+
|
|
237
|
+
# Your processing logic
|
|
238
|
+
analyze_filing(filing)
|
|
239
|
+
store_in_database(filing)
|
|
240
|
+
send_notifications(filing)
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Stream handler - just enqueue jobs, don't block
|
|
245
|
+
client.stream.subscribe do |filing|
|
|
246
|
+
# Enqueue and return immediately - don't block the reactor
|
|
247
|
+
ProcessFilingWorker.perform_async(
|
|
248
|
+
filing.accession_no,
|
|
249
|
+
filing.ticker,
|
|
250
|
+
filing.form_type,
|
|
251
|
+
filing.filed_at.iso8601
|
|
252
|
+
)
|
|
253
|
+
end
|
|
254
|
+
CODE
|
|
255
|
+
|
|
256
|
+
# ActiveJob integration
|
|
257
|
+
puts "\nPattern: ActiveJob integration"
|
|
258
|
+
puts <<~CODE
|
|
259
|
+
# Job class (in app/jobs/process_filing_job.rb)
|
|
260
|
+
class ProcessFilingJob < ApplicationJob
|
|
261
|
+
queue_as :sec_filings
|
|
262
|
+
|
|
263
|
+
def perform(accession_no:, ticker:, form_type:)
|
|
264
|
+
# Your processing logic here
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Stream handler
|
|
269
|
+
client.stream.subscribe do |filing|
|
|
270
|
+
ProcessFilingJob.perform_later(
|
|
271
|
+
accession_no: filing.accession_no,
|
|
272
|
+
ticker: filing.ticker,
|
|
273
|
+
form_type: filing.form_type
|
|
274
|
+
)
|
|
275
|
+
end
|
|
276
|
+
CODE
|
|
277
|
+
|
|
278
|
+
# Thread pool for non-Rails apps
|
|
279
|
+
puts "\nPattern: Thread pool processing"
|
|
280
|
+
puts <<~CODE
|
|
281
|
+
require 'concurrent'
|
|
282
|
+
|
|
283
|
+
pool = Concurrent::ThreadPoolExecutor.new(
|
|
284
|
+
min_threads: 2,
|
|
285
|
+
max_threads: 10,
|
|
286
|
+
max_queue: 100
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
client.stream.subscribe do |filing|
|
|
290
|
+
pool.post do
|
|
291
|
+
process_filing(filing)
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
CODE
|
|
295
|
+
|
|
296
|
+
# =============================================================================
|
|
297
|
+
# SECTION 6: Latency Monitoring and Alerting
|
|
298
|
+
# =============================================================================
|
|
299
|
+
|
|
300
|
+
puts "\n" + "=" * 60
|
|
301
|
+
puts "SECTION 6: Latency Monitoring"
|
|
302
|
+
puts "=" * 60
|
|
303
|
+
|
|
304
|
+
# Monitor filing delivery latency
|
|
305
|
+
puts "\nPattern: Latency monitoring with on_filing callback"
|
|
306
|
+
puts <<~CODE
|
|
307
|
+
config = SecApi::Config.new(
|
|
308
|
+
api_key: ENV.fetch("SECAPI_API_KEY"),
|
|
309
|
+
|
|
310
|
+
# Latency warning threshold (2 minutes = 120 seconds)
|
|
311
|
+
stream_latency_warning_threshold: 120.0,
|
|
312
|
+
|
|
313
|
+
# Called for every filing (before filtering)
|
|
314
|
+
on_filing: ->(filing:, latency_ms:, received_at:) {
|
|
315
|
+
# Record latency metrics
|
|
316
|
+
StatsD.histogram("sec_api.stream.latency_ms", latency_ms)
|
|
317
|
+
StatsD.increment("sec_api.stream.filings_received")
|
|
318
|
+
|
|
319
|
+
# Alert on high latency
|
|
320
|
+
if latency_ms > 120_000 # 2 minutes in ms
|
|
321
|
+
AlertService.warn("SEC filing latency exceeded 2 minutes", {
|
|
322
|
+
accession_no: filing.accession_no,
|
|
323
|
+
latency_ms: latency_ms
|
|
324
|
+
})
|
|
325
|
+
end
|
|
326
|
+
}
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
client = SecApi::Client.new(config)
|
|
330
|
+
CODE
|
|
331
|
+
|
|
332
|
+
# Access latency from filing object
|
|
333
|
+
puts "\nPattern: Latency from filing object"
|
|
334
|
+
puts <<~CODE
|
|
335
|
+
client.stream.subscribe do |filing|
|
|
336
|
+
puts "Filing latency: \#{filing.latency_ms}ms"
|
|
337
|
+
puts "Filed at: \#{filing.filed_at}"
|
|
338
|
+
puts "Received at: \#{filing.received_at}"
|
|
339
|
+
|
|
340
|
+
# filing.latency_seconds is also available
|
|
341
|
+
if filing.latency_seconds > 60
|
|
342
|
+
puts "Warning: High latency (\#{filing.latency_seconds}s)"
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
CODE
|
|
346
|
+
|
|
347
|
+
# Structured logging for latency
|
|
348
|
+
puts "\nPattern: Structured latency logging"
|
|
349
|
+
puts <<~CODE
|
|
350
|
+
config = SecApi::Config.new(
|
|
351
|
+
api_key: ENV.fetch("SECAPI_API_KEY"),
|
|
352
|
+
logger: Rails.logger,
|
|
353
|
+
log_level: :info # Logs JSON events automatically
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
# With logger configured, the stream automatically logs:
|
|
357
|
+
# {"event":"secapi.stream.filing_received","latency_ms":1500,...}
|
|
358
|
+
# {"event":"secapi.stream.latency_warning","latency_ms":130000,...}
|
|
359
|
+
CODE
|
|
360
|
+
|
|
361
|
+
# =============================================================================
|
|
362
|
+
# SECTION 7: Graceful Shutdown Pattern
|
|
363
|
+
# =============================================================================
|
|
364
|
+
|
|
365
|
+
puts "\n" + "=" * 60
|
|
366
|
+
puts "SECTION 7: Graceful Shutdown"
|
|
367
|
+
puts "=" * 60
|
|
368
|
+
|
|
369
|
+
# Clean shutdown with signal handling
|
|
370
|
+
puts "\nPattern: Signal-based graceful shutdown"
|
|
371
|
+
puts <<~CODE
|
|
372
|
+
client = SecApi::Client.new
|
|
373
|
+
stream = client.stream
|
|
374
|
+
|
|
375
|
+
# Handle shutdown signals
|
|
376
|
+
shutdown = false
|
|
377
|
+
|
|
378
|
+
%w[INT TERM].each do |signal|
|
|
379
|
+
Signal.trap(signal) do
|
|
380
|
+
puts "\\nReceived \#{signal}, shutting down..."
|
|
381
|
+
shutdown = true
|
|
382
|
+
stream.close # Close the WebSocket connection
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# Start streaming in a way that respects shutdown
|
|
387
|
+
Thread.new do
|
|
388
|
+
stream.subscribe do |filing|
|
|
389
|
+
break if shutdown
|
|
390
|
+
process_filing(filing)
|
|
391
|
+
end
|
|
392
|
+
rescue SecApi::NetworkError => e
|
|
393
|
+
# Connection closed or network error
|
|
394
|
+
puts "Stream disconnected: \#{e.message}" unless shutdown
|
|
395
|
+
end.join
|
|
396
|
+
CODE
|
|
397
|
+
|
|
398
|
+
# Check connection status
|
|
399
|
+
puts "\nPattern: Connection status monitoring"
|
|
400
|
+
puts <<~CODE
|
|
401
|
+
stream = client.stream
|
|
402
|
+
|
|
403
|
+
# Start streaming in background
|
|
404
|
+
streaming_thread = Thread.new do
|
|
405
|
+
stream.subscribe(tickers: ["AAPL"]) { |f| process(f) }
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
# Monitor connection status from main thread
|
|
409
|
+
loop do
|
|
410
|
+
if stream.connected?
|
|
411
|
+
puts "Stream connected, filters: \#{stream.filters}"
|
|
412
|
+
else
|
|
413
|
+
puts "Stream disconnected"
|
|
414
|
+
end
|
|
415
|
+
sleep 30
|
|
416
|
+
end
|
|
417
|
+
CODE
|
|
418
|
+
|
|
419
|
+
# =============================================================================
|
|
420
|
+
# SECTION 8: Complete Example
|
|
421
|
+
# =============================================================================
|
|
422
|
+
|
|
423
|
+
puts "\n" + "=" * 60
|
|
424
|
+
puts "SECTION 8: Complete Production Example"
|
|
425
|
+
puts "=" * 60
|
|
426
|
+
|
|
427
|
+
puts "\nComplete production-ready streaming setup:"
|
|
428
|
+
puts <<~CODE
|
|
429
|
+
require "sec_api"
|
|
430
|
+
|
|
431
|
+
# Configure with all observability features
|
|
432
|
+
config = SecApi::Config.new(
|
|
433
|
+
api_key: ENV.fetch("SECAPI_API_KEY"),
|
|
434
|
+
|
|
435
|
+
# Logging
|
|
436
|
+
logger: Rails.logger,
|
|
437
|
+
log_level: :info,
|
|
438
|
+
|
|
439
|
+
# Latency monitoring
|
|
440
|
+
stream_latency_warning_threshold: 120.0,
|
|
441
|
+
|
|
442
|
+
# Reconnection
|
|
443
|
+
stream_max_reconnect_attempts: 10,
|
|
444
|
+
stream_initial_reconnect_delay: 1.0,
|
|
445
|
+
stream_max_reconnect_delay: 60.0,
|
|
446
|
+
|
|
447
|
+
# Callbacks for observability
|
|
448
|
+
on_filing: ->(filing:, latency_ms:, received_at:) {
|
|
449
|
+
StatsD.histogram("sec_api.stream.latency_ms", latency_ms)
|
|
450
|
+
},
|
|
451
|
+
|
|
452
|
+
on_reconnect: ->(info) {
|
|
453
|
+
StatsD.increment("sec_api.stream.reconnected")
|
|
454
|
+
StatsD.gauge("sec_api.stream.downtime_seconds", info[:downtime_seconds])
|
|
455
|
+
},
|
|
456
|
+
|
|
457
|
+
on_callback_error: ->(info) {
|
|
458
|
+
Bugsnag.notify(info[:error], {
|
|
459
|
+
accession_no: info[:accession_no],
|
|
460
|
+
ticker: info[:ticker]
|
|
461
|
+
})
|
|
462
|
+
}
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
client = SecApi::Client.new(config)
|
|
466
|
+
|
|
467
|
+
# Track for backfill detection
|
|
468
|
+
last_received = nil
|
|
469
|
+
|
|
470
|
+
# Subscribe with filtering
|
|
471
|
+
client.stream.subscribe(
|
|
472
|
+
tickers: %w[AAPL TSLA MSFT GOOGL AMZN],
|
|
473
|
+
form_types: %w[10-K 10-Q 8-K]
|
|
474
|
+
) do |filing|
|
|
475
|
+
last_received = Time.now
|
|
476
|
+
|
|
477
|
+
# Enqueue for background processing
|
|
478
|
+
ProcessFilingWorker.perform_async(
|
|
479
|
+
filing.accession_no,
|
|
480
|
+
filing.ticker,
|
|
481
|
+
filing.form_type,
|
|
482
|
+
filing.filed_at.iso8601
|
|
483
|
+
)
|
|
484
|
+
end
|
|
485
|
+
CODE
|
|
486
|
+
|
|
487
|
+
puts "\n" + "=" * 60
|
|
488
|
+
puts "Examples completed!"
|
|
489
|
+
puts "=" * 60
|
|
490
|
+
puts "\nNote: These examples demonstrate patterns."
|
|
491
|
+
puts "Uncomment the code blocks to run them with a live connection."
|