openfigi_ruby 0.1.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/.yardopts +6 -0
- data/CLAUDE.md +76 -0
- data/LICENSE.txt +21 -0
- data/README.md +174 -0
- data/Rakefile +14 -0
- data/lib/openfigi_ruby/client.rb +212 -0
- data/lib/openfigi_ruby/configuration.rb +25 -0
- data/lib/openfigi_ruby/error.rb +34 -0
- data/lib/openfigi_ruby/figi_result.rb +42 -0
- data/lib/openfigi_ruby/mapping_result.rb +20 -0
- data/lib/openfigi_ruby/search_result.rb +29 -0
- data/lib/openfigi_ruby/version.rb +7 -0
- data/lib/openfigi_ruby.rb +43 -0
- data/sig/openfigi_ruby.rbs +4 -0
- metadata +115 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: bb96df5aae03995901f25a8e042064c881d7d43bd2763d2164feca902588c797
|
|
4
|
+
data.tar.gz: b7d083008c394a57fd000480eabdf52348c7f207c1a17a7db9435112402a8aea
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: a027a62e72d4f2c7e18f2da594fe15576f9fcc49d5ec1d443ccdaec26457fdd4924e548dade296e2ba701ce1d585ab98e733567436a3403a82d0549c9abeaae5
|
|
7
|
+
data.tar.gz: 0b147217f140a7bd5d810e148f474c4fa0c51bcd5841924234b331f19778179079f478aae1d1d28d9dfcb24f00f09487e0c40395e07ca4333c440e1754c3e624
|
data/.yardopts
ADDED
data/CLAUDE.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bin/setup # Install dependencies
|
|
9
|
+
bin/console # Open IRB session with the gem loaded (for manual testing)
|
|
10
|
+
bundle exec rake # Run default rake tasks
|
|
11
|
+
gem build # Build the .gem file
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
No test framework is configured. If tests are added, update this file with the test command.
|
|
15
|
+
|
|
16
|
+
## Architecture
|
|
17
|
+
|
|
18
|
+
A Ruby gem wrapping the [OpenFIGI V3 API](https://www.openfigi.com/api) — a financial data service for mapping identifiers (ISIN, CUSIP, ticker, etc.) to FIGIs (Financial Instrument Global Identifiers). V3 only; V2 is sunsetting.
|
|
19
|
+
|
|
20
|
+
**Base URL:** `https://api.openfigi.com/v3`
|
|
21
|
+
**Auth:** optional `X-OPENFIGI-APIKEY` header
|
|
22
|
+
**Rate limits:** 25 req/min (unauthenticated) · 25 req/6s (authenticated) for mapping; lower limits for search/filter
|
|
23
|
+
|
|
24
|
+
### Module layout
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
lib/openfigi_ruby.rb # Entry point: requires all sub-files, exposes .configure
|
|
28
|
+
lib/openfigi_ruby/
|
|
29
|
+
version.rb # VERSION constant
|
|
30
|
+
configuration.rb # Configuration (api_key, open_timeout, read_timeout)
|
|
31
|
+
error.rb # Error hierarchy (see below)
|
|
32
|
+
client.rb # HTTP client — all API methods live here
|
|
33
|
+
figi_result.rb # FigiResult Struct (one matched instrument)
|
|
34
|
+
mapping_result.rb # MappingResult Struct (data array or warning)
|
|
35
|
+
search_result.rb # SearchResult and FilterResult Structs
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Client API
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
OpenfigiRuby.configure { |c| c.api_key = ENV["OPENFIGI_API_KEY"] }
|
|
42
|
+
client = OpenfigiRuby::Client.new # uses global config; accepts api_key: override
|
|
43
|
+
|
|
44
|
+
client.mapping([{ id_type: "ID_ISIN", id_value: "US4592001014" }])
|
|
45
|
+
# => [MappingResult(data: [FigiResult(...)], warning: nil)]
|
|
46
|
+
|
|
47
|
+
client.mapping_values(:id_type)
|
|
48
|
+
# => ["ID_ISIN", "ID_CUSIP", ...]
|
|
49
|
+
|
|
50
|
+
client.search(query: "Apple", exch_code: "US")
|
|
51
|
+
# => SearchResult(data: [...], next_page: "...", error: nil)
|
|
52
|
+
|
|
53
|
+
client.filter(query: "Apple", currency: "USD")
|
|
54
|
+
# => FilterResult(data: [...], next_page: "...", total: 42, error: nil)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
All input hashes use **snake_case** symbol keys; the client converts to camelCase for the API. Response structs use snake_case accessors. `next_page` holds the pagination token (the API field is `next`).
|
|
58
|
+
|
|
59
|
+
### Error hierarchy
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
OpenfigiRuby::Error
|
|
63
|
+
└─ ApiError (status_code:, body: attributes)
|
|
64
|
+
├─ AuthenticationError # HTTP 401
|
|
65
|
+
├─ RateLimitError # HTTP 429
|
|
66
|
+
├─ InvalidRequestError # HTTP 400
|
|
67
|
+
└─ ServerError # HTTP 500/503
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Conventions
|
|
71
|
+
|
|
72
|
+
- All files use `# frozen_string_literal: true`
|
|
73
|
+
- Ruby >= 3.1.0 required
|
|
74
|
+
- No runtime dependencies — uses only `net/http`, `json`, `uri` from stdlib
|
|
75
|
+
- Runtime dependencies go in `openfigi_ruby.gemspec`; dev-only in `Gemfile`
|
|
76
|
+
- Version is the single source of truth in `lib/openfigi_ruby/version.rb`
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Phong Si
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# OpenfigiRuby
|
|
2
|
+
|
|
3
|
+
Ruby client for the [OpenFIGI V3 API](https://www.openfigi.com/api). Maps financial identifiers (ISIN, CUSIP, ticker, etc.) to FIGIs (Financial Instrument Global Identifiers), with support for keyword search and filtering.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bundle add openfigi_ruby
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or add to your Gemfile manually:
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
gem "openfigi_ruby"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
### Configuration
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
OpenfigiRuby.configure do |config|
|
|
23
|
+
config.api_key = ENV["OPENFIGI_API_KEY"] # optional but recommended for higher rate limits
|
|
24
|
+
config.open_timeout = 10 # seconds (default: 10)
|
|
25
|
+
config.read_timeout = 30 # seconds (default: 30)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
client = OpenfigiRuby::Client.new
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
You can also pass an API key per-client without touching global config:
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
client = OpenfigiRuby::Client.new(api_key: "your_key")
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
### Mapping identifiers to FIGIs
|
|
40
|
+
|
|
41
|
+
`mapping` accepts up to 10 jobs per request (100 with an API key) and returns one result per job.
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
results = client.mapping([
|
|
45
|
+
{ id_type: "ID_ISIN", id_value: "US0378331005" },
|
|
46
|
+
{ id_type: "ID_CUSIP", id_value: "037833100" },
|
|
47
|
+
{ id_type: "TICKER", id_value: "AAPL", exch_code: "US" },
|
|
48
|
+
])
|
|
49
|
+
|
|
50
|
+
results.each do |result|
|
|
51
|
+
if result.found?
|
|
52
|
+
result.data.each do |figi|
|
|
53
|
+
puts "#{figi.name} — #{figi.figi} (#{figi.exch_code})"
|
|
54
|
+
end
|
|
55
|
+
else
|
|
56
|
+
puts "No match: #{result.warning}"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Optional filters per job:**
|
|
62
|
+
|
|
63
|
+
| Key | Description |
|
|
64
|
+
|---|---|
|
|
65
|
+
| `:exch_code` | Exchange code (e.g. `"US"`, `"LN"`) |
|
|
66
|
+
| `:mic_code` | Market Identifier Code |
|
|
67
|
+
| `:currency` | Currency code (e.g. `"USD"`) |
|
|
68
|
+
| `:market_sec_des` | Market sector description |
|
|
69
|
+
| `:security_type` | Primary security type |
|
|
70
|
+
| `:security_type2` | Secondary security type (required for `BASE_TICKER`/`ID_EXCH_SYMBOL`) |
|
|
71
|
+
| `:include_unlisted_equities` | Boolean |
|
|
72
|
+
| `:option_type` | `"Put"` or `"Call"` |
|
|
73
|
+
| `:strike` | Two-element numeric range, e.g. `[100, 150]` |
|
|
74
|
+
| `:expiration` | Two-element date range, e.g. `["2024-01-01", "2024-12-31"]` |
|
|
75
|
+
| `:maturity` | Two-element date range (required for Pool) |
|
|
76
|
+
| `:state_code` | US state code |
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
### Fetching valid enum values
|
|
81
|
+
|
|
82
|
+
Look up the accepted values for any filterable field:
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
client.mapping_values(:id_type)
|
|
86
|
+
# => ["ID_ISIN", "ID_CUSIP", "TICKER", "ID_BB_GLOBAL", ...]
|
|
87
|
+
|
|
88
|
+
client.mapping_values(:exch_code)
|
|
89
|
+
# => ["US", "LN", "UN", ...]
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Valid keys: `:id_type`, `:exch_code`, `:mic_code`, `:currency`, `:market_sec_des`, `:security_type`, `:security_type2`
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
### Keyword search
|
|
97
|
+
|
|
98
|
+
Search for FIGIs by name or ticker. Results are paginated.
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
result = client.search(query: "Apple", exch_code: "US", security_type: "Common Stock")
|
|
102
|
+
|
|
103
|
+
result.data.each { |figi| puts "#{figi.ticker} — #{figi.figi}" }
|
|
104
|
+
|
|
105
|
+
# Fetch the next page
|
|
106
|
+
if result.next_page
|
|
107
|
+
result = client.search(query: "Apple", exch_code: "US", start: result.next_page)
|
|
108
|
+
end
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
### Filter
|
|
114
|
+
|
|
115
|
+
Like search, but results are sorted alphabetically by FIGI and include a total count. Query is optional.
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
result = client.filter(currency: "USD", security_type: "Common Stock")
|
|
119
|
+
|
|
120
|
+
puts "#{result.total} total matches"
|
|
121
|
+
result.data.each { |figi| puts figi.figi }
|
|
122
|
+
|
|
123
|
+
# Paginate
|
|
124
|
+
while result.next_page
|
|
125
|
+
result = client.filter(currency: "USD", security_type: "Common Stock", start: result.next_page)
|
|
126
|
+
result.data.each { |figi| puts figi.figi }
|
|
127
|
+
end
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
### Error handling
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
begin
|
|
136
|
+
results = client.mapping([{ id_type: "TICKER", id_value: "AAPL" }])
|
|
137
|
+
rescue OpenfigiRuby::AuthenticationError
|
|
138
|
+
# HTTP 401 — check your API key
|
|
139
|
+
rescue OpenfigiRuby::RateLimitError
|
|
140
|
+
# HTTP 429 — back off and retry
|
|
141
|
+
rescue OpenfigiRuby::InvalidRequestError => e
|
|
142
|
+
# HTTP 400 — malformed request
|
|
143
|
+
puts e.body
|
|
144
|
+
rescue OpenfigiRuby::ServerError
|
|
145
|
+
# HTTP 500/503 — retry with exponential backoff
|
|
146
|
+
rescue OpenfigiRuby::ApiError => e
|
|
147
|
+
# catch-all for any other non-200 response
|
|
148
|
+
puts "#{e.status_code}: #{e.message}"
|
|
149
|
+
end
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
### Rate limits
|
|
155
|
+
|
|
156
|
+
| | Without API key | With API key |
|
|
157
|
+
|---|---|---|
|
|
158
|
+
| **Mapping** | 25 req/min, 10 jobs/req | 25 req/6s, 100 jobs/req |
|
|
159
|
+
| **Search/Filter** | 5 req/min | 20 req/min |
|
|
160
|
+
|
|
161
|
+
Get a free API key at [openfigi.com](https://www.openfigi.com/api#get-started).
|
|
162
|
+
|
|
163
|
+
## Development
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
bin/setup # install dependencies
|
|
167
|
+
bin/console # interactive prompt with the gem loaded
|
|
168
|
+
bundle exec rake # run tests
|
|
169
|
+
bundle exec yard server # browse docs at http://localhost:8808
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## License
|
|
173
|
+
|
|
174
|
+
[MIT](LICENSE.txt)
|
data/Rakefile
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "rake/testtask"
|
|
5
|
+
require "yard"
|
|
6
|
+
|
|
7
|
+
Rake::TestTask.new(:test) do |t|
|
|
8
|
+
t.libs << "test"
|
|
9
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
YARD::Rake::YardocTask.new(:doc)
|
|
13
|
+
|
|
14
|
+
task default: :test
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module OpenfigiRuby
|
|
8
|
+
# HTTP client for the OpenFIGI V3 API.
|
|
9
|
+
#
|
|
10
|
+
# Instantiate with an optional API key override, or configure globally via
|
|
11
|
+
# {OpenfigiRuby.configure} and call +Client.new+ with no arguments.
|
|
12
|
+
class Client
|
|
13
|
+
# Valid field names accepted by {#mapping_values}.
|
|
14
|
+
# @api private
|
|
15
|
+
MAPPING_VALUE_KEYS = %w[idType exchCode micCode currency marketSecDes securityType securityType2].freeze
|
|
16
|
+
|
|
17
|
+
# @param api_key [String, nil] overrides the globally configured key
|
|
18
|
+
# @param open_timeout [Integer] seconds before the connection times out
|
|
19
|
+
# @param read_timeout [Integer] seconds before the read times out
|
|
20
|
+
def initialize(
|
|
21
|
+
api_key: OpenfigiRuby.configuration.api_key,
|
|
22
|
+
open_timeout: OpenfigiRuby.configuration.open_timeout,
|
|
23
|
+
read_timeout: OpenfigiRuby.configuration.read_timeout
|
|
24
|
+
)
|
|
25
|
+
@api_key = api_key
|
|
26
|
+
@open_timeout = open_timeout
|
|
27
|
+
@read_timeout = read_timeout
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Maps third-party identifiers to FIGIs (POST /v3/mapping).
|
|
31
|
+
#
|
|
32
|
+
# @param jobs [Array<Hash>] each hash must include +:id_type+ and +:id_value+.
|
|
33
|
+
# Optional keys: +:exch_code+, +:mic_code+, +:currency+, +:market_sec_des+,
|
|
34
|
+
# +:security_type+, +:security_type2+, +:include_unlisted_equities+,
|
|
35
|
+
# +:option_type+, +:strike+, +:contract_size+, +:coupon+,
|
|
36
|
+
# +:expiration+, +:maturity+, +:state_code+.
|
|
37
|
+
# @return [Array<MappingResult>] one result per input job
|
|
38
|
+
#
|
|
39
|
+
# @example
|
|
40
|
+
# client.mapping([
|
|
41
|
+
# { id_type: "ID_ISIN", id_value: "US4592001014" },
|
|
42
|
+
# { id_type: "TICKER", id_value: "AAPL", exch_code: "US" }
|
|
43
|
+
# ])
|
|
44
|
+
def mapping(jobs)
|
|
45
|
+
body = jobs.map { |job| serialize(job) }
|
|
46
|
+
response = post("/mapping", body)
|
|
47
|
+
response.map { |item| parse_mapping_result(item) }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Returns the set of valid values for a filterable field (GET /v3/mapping/values/:key).
|
|
51
|
+
#
|
|
52
|
+
# @param key [String, Symbol] snake_case or camelCase field name.
|
|
53
|
+
# Valid values: :id_type, :exch_code, :mic_code, :currency,
|
|
54
|
+
# :market_sec_des, :security_type, :security_type2
|
|
55
|
+
# @return [Array<String>]
|
|
56
|
+
#
|
|
57
|
+
# @example
|
|
58
|
+
# client.mapping_values(:id_type)
|
|
59
|
+
def mapping_values(key)
|
|
60
|
+
api_key = camelize(key.to_s)
|
|
61
|
+
response = get("/mapping/values/#{api_key}")
|
|
62
|
+
response["values"]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Searches for FIGIs by keyword (POST /v3/search).
|
|
66
|
+
#
|
|
67
|
+
# @param query [String] search terms
|
|
68
|
+
# @param start [String, nil] pagination token from a previous {SearchResult#next_page}
|
|
69
|
+
# @param filters [Hash] optional snake_case filter keys (same as mapping job filters)
|
|
70
|
+
# @return [SearchResult]
|
|
71
|
+
#
|
|
72
|
+
# @example
|
|
73
|
+
# result = client.search(query: "Apple", exch_code: "US")
|
|
74
|
+
# result.data #=> [#<struct OpenfigiRuby::FigiResult ...>, ...]
|
|
75
|
+
# result.next_page #=> "BBG..." or nil
|
|
76
|
+
def search(query:, start: nil, **filters)
|
|
77
|
+
body = { "query" => query }
|
|
78
|
+
body["start"] = start if start
|
|
79
|
+
body.merge!(serialize(filters))
|
|
80
|
+
response = post("/search", body)
|
|
81
|
+
parse_search_result(response)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Filters FIGIs with alphabetical ordering and a total count (POST /v3/filter).
|
|
85
|
+
#
|
|
86
|
+
# @param query [String, nil] optional search terms
|
|
87
|
+
# @param start [String, nil] pagination token from a previous {FilterResult#next_page}
|
|
88
|
+
# @param filters [Hash] optional snake_case filter keys
|
|
89
|
+
# @return [FilterResult]
|
|
90
|
+
def filter(query: nil, start: nil, **filters)
|
|
91
|
+
body = {}
|
|
92
|
+
body["query"] = query if query
|
|
93
|
+
body["start"] = start if start
|
|
94
|
+
body.merge!(serialize(filters))
|
|
95
|
+
response = post("/filter", body)
|
|
96
|
+
parse_filter_result(response)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
# OpenFIGI V3 API base URL.
|
|
102
|
+
# @api private
|
|
103
|
+
BASE_URL = "https://api.openfigi.com/v3"
|
|
104
|
+
|
|
105
|
+
def post(path, body)
|
|
106
|
+
uri = URI("#{BASE_URL}#{path}")
|
|
107
|
+
http = build_http(uri)
|
|
108
|
+
request = Net::HTTP::Post.new(uri)
|
|
109
|
+
apply_headers(request)
|
|
110
|
+
request.body = body.to_json
|
|
111
|
+
handle_response(http.request(request))
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def get(path)
|
|
115
|
+
uri = URI("#{BASE_URL}#{path}")
|
|
116
|
+
http = build_http(uri)
|
|
117
|
+
request = Net::HTTP::Get.new(uri)
|
|
118
|
+
apply_headers(request)
|
|
119
|
+
handle_response(http.request(request))
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def build_http(uri)
|
|
123
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
124
|
+
http.use_ssl = uri.scheme == "https"
|
|
125
|
+
http.open_timeout = @open_timeout
|
|
126
|
+
http.read_timeout = @read_timeout
|
|
127
|
+
http
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def apply_headers(request)
|
|
131
|
+
request["Content-Type"] = "application/json"
|
|
132
|
+
request["X-OPENFIGI-APIKEY"] = @api_key if @api_key
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def handle_response(response)
|
|
136
|
+
body = response.body
|
|
137
|
+
code = response.code.to_i
|
|
138
|
+
|
|
139
|
+
case code
|
|
140
|
+
when 200
|
|
141
|
+
JSON.parse(body)
|
|
142
|
+
when 400
|
|
143
|
+
raise InvalidRequestError.new("Invalid request payload", status_code: code, body: body)
|
|
144
|
+
when 401
|
|
145
|
+
raise AuthenticationError.new("Invalid API key", status_code: code, body: body)
|
|
146
|
+
when 429
|
|
147
|
+
raise RateLimitError.new("Rate limit exceeded", status_code: code, body: body)
|
|
148
|
+
when 500, 503
|
|
149
|
+
raise ServerError.new("Server error (#{code})", status_code: code, body: body)
|
|
150
|
+
else
|
|
151
|
+
raise ApiError.new("Unexpected response (#{code})", status_code: code, body: body)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Converts a hash with snake_case symbol keys to camelCase string keys,
|
|
156
|
+
# dropping any nil values.
|
|
157
|
+
def serialize(hash)
|
|
158
|
+
hash.each_with_object({}) do |(key, value), result|
|
|
159
|
+
next if value.nil?
|
|
160
|
+
|
|
161
|
+
result[camelize(key.to_s)] = value
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# "id_type" => "idType", "security_type2" => "securityType2"
|
|
166
|
+
def camelize(snake_str)
|
|
167
|
+
parts = snake_str.split("_")
|
|
168
|
+
parts[0] + parts[1..].map(&:capitalize).join
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def parse_mapping_result(item)
|
|
172
|
+
if item.key?("data")
|
|
173
|
+
MappingResult.new(data: item["data"].map { |r| build_figi_result(r) })
|
|
174
|
+
else
|
|
175
|
+
MappingResult.new(warning: item["warning"])
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def parse_search_result(response)
|
|
180
|
+
SearchResult.new(
|
|
181
|
+
data: Array(response["data"]).map { |r| build_figi_result(r) },
|
|
182
|
+
next_page: response["next"],
|
|
183
|
+
error: response["error"]
|
|
184
|
+
)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def parse_filter_result(response)
|
|
188
|
+
FilterResult.new(
|
|
189
|
+
data: Array(response["data"]).map { |r| build_figi_result(r) },
|
|
190
|
+
next_page: response["next"],
|
|
191
|
+
total: response["total"],
|
|
192
|
+
error: response["error"]
|
|
193
|
+
)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def build_figi_result(hash)
|
|
197
|
+
FigiResult.new(
|
|
198
|
+
figi: hash["figi"],
|
|
199
|
+
security_type: hash["securityType"],
|
|
200
|
+
market_sector: hash["marketSector"],
|
|
201
|
+
ticker: hash["ticker"],
|
|
202
|
+
name: hash["name"],
|
|
203
|
+
exch_code: hash["exchCode"],
|
|
204
|
+
share_class_figi: hash["shareClassFIGI"],
|
|
205
|
+
composite_figi: hash["compositeFIGI"],
|
|
206
|
+
security_type2: hash["securityType2"],
|
|
207
|
+
security_description: hash["securityDescription"],
|
|
208
|
+
metadata: hash["metadata"]
|
|
209
|
+
)
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenfigiRuby
|
|
4
|
+
# Holds configuration for the gem. Set values via {OpenfigiRuby.configure}.
|
|
5
|
+
#
|
|
6
|
+
# @!attribute [rw] api_key
|
|
7
|
+
# @return [String, nil] OpenFIGI API key. Without a key, stricter rate limits apply.
|
|
8
|
+
# @!attribute [rw] open_timeout
|
|
9
|
+
# @return [Integer] seconds to wait when opening a connection (default: 10)
|
|
10
|
+
# @!attribute [rw] read_timeout
|
|
11
|
+
# @return [Integer] seconds to wait for a response (default: 30)
|
|
12
|
+
class Configuration
|
|
13
|
+
# OpenFIGI V3 API base URL.
|
|
14
|
+
# @api private
|
|
15
|
+
BASE_URL = "https://api.openfigi.com/v3"
|
|
16
|
+
|
|
17
|
+
attr_accessor :api_key, :open_timeout, :read_timeout
|
|
18
|
+
|
|
19
|
+
def initialize
|
|
20
|
+
@api_key = nil
|
|
21
|
+
@open_timeout = 10
|
|
22
|
+
@read_timeout = 30
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenfigiRuby
|
|
4
|
+
# Base error class for all OpenfigiRuby errors.
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# Raised when the API returns a non-successful HTTP status.
|
|
8
|
+
#
|
|
9
|
+
# @!attribute [r] status_code
|
|
10
|
+
# @return [Integer, nil] HTTP status code from the response
|
|
11
|
+
# @!attribute [r] body
|
|
12
|
+
# @return [String, nil] raw response body
|
|
13
|
+
class ApiError < Error
|
|
14
|
+
attr_reader :status_code, :body
|
|
15
|
+
|
|
16
|
+
def initialize(message = nil, status_code: nil, body: nil)
|
|
17
|
+
super(message)
|
|
18
|
+
@status_code = status_code
|
|
19
|
+
@body = body
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Raised on HTTP 401 — invalid or missing API key.
|
|
24
|
+
class AuthenticationError < ApiError; end
|
|
25
|
+
|
|
26
|
+
# Raised on HTTP 429 — rate limit exceeded.
|
|
27
|
+
class RateLimitError < ApiError; end
|
|
28
|
+
|
|
29
|
+
# Raised on HTTP 400 — malformed request payload.
|
|
30
|
+
class InvalidRequestError < ApiError; end
|
|
31
|
+
|
|
32
|
+
# Raised on HTTP 500/503 — transient server errors (retry with backoff).
|
|
33
|
+
class ServerError < ApiError; end
|
|
34
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenfigiRuby
|
|
4
|
+
# Represents a single financial instrument returned by the OpenFIGI API.
|
|
5
|
+
#
|
|
6
|
+
# @!attribute [r] figi
|
|
7
|
+
# @return [String] the Financial Instrument Global Identifier (FIGI)
|
|
8
|
+
# @!attribute [r] security_type
|
|
9
|
+
# @return [String, nil] primary security classification (e.g. "Common Stock", "ETP")
|
|
10
|
+
# @!attribute [r] market_sector
|
|
11
|
+
# @return [String, nil] market sector (e.g. "Equity", "Corp", "Govt")
|
|
12
|
+
# @!attribute [r] ticker
|
|
13
|
+
# @return [String, nil] exchange ticker symbol
|
|
14
|
+
# @!attribute [r] name
|
|
15
|
+
# @return [String, nil] instrument name
|
|
16
|
+
# @!attribute [r] exch_code
|
|
17
|
+
# @return [String, nil] exchange code (e.g. "US", "LN")
|
|
18
|
+
# @!attribute [r] share_class_figi
|
|
19
|
+
# @return [String, nil] share class-level FIGI
|
|
20
|
+
# @!attribute [r] composite_figi
|
|
21
|
+
# @return [String, nil] composite FIGI (aggregates listings across exchanges)
|
|
22
|
+
# @!attribute [r] security_type2
|
|
23
|
+
# @return [String, nil] secondary security classification
|
|
24
|
+
# @!attribute [r] security_description
|
|
25
|
+
# @return [String, nil] short description of the security
|
|
26
|
+
# @!attribute [r] metadata
|
|
27
|
+
# @return [String, nil] optional metadata string returned by the API
|
|
28
|
+
FigiResult = Struct.new(
|
|
29
|
+
:figi,
|
|
30
|
+
:security_type,
|
|
31
|
+
:market_sector,
|
|
32
|
+
:ticker,
|
|
33
|
+
:name,
|
|
34
|
+
:exch_code,
|
|
35
|
+
:share_class_figi,
|
|
36
|
+
:composite_figi,
|
|
37
|
+
:security_type2,
|
|
38
|
+
:security_description,
|
|
39
|
+
:metadata,
|
|
40
|
+
keyword_init: true
|
|
41
|
+
)
|
|
42
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenfigiRuby
|
|
4
|
+
# Result for a single job in a bulk mapping request.
|
|
5
|
+
#
|
|
6
|
+
# On success, {data} is an array of {FigiResult} objects and {warning} is nil.
|
|
7
|
+
# When no match is found, {data} is nil and {warning} holds the API message.
|
|
8
|
+
#
|
|
9
|
+
# @!attribute [r] data
|
|
10
|
+
# @return [Array<FigiResult>, nil] matched instruments, or nil when no match was found
|
|
11
|
+
# @!attribute [r] warning
|
|
12
|
+
# @return [String, nil] API warning message when no identifier was found
|
|
13
|
+
MappingResult = Struct.new(:data, :warning, keyword_init: true) do
|
|
14
|
+
# Returns true if the job matched at least one instrument.
|
|
15
|
+
# @return [Boolean]
|
|
16
|
+
def found?
|
|
17
|
+
!data.nil? && !data.empty?
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenfigiRuby
|
|
4
|
+
# Result from POST /v3/search.
|
|
5
|
+
#
|
|
6
|
+
# @!attribute [r] data
|
|
7
|
+
# @return [Array<FigiResult>] instruments matching the search query for this page
|
|
8
|
+
# @!attribute [r] next_page
|
|
9
|
+
# @return [String, nil] opaque pagination token; pass as +start:+ to {Client#search} to fetch
|
|
10
|
+
# the next page. nil when this is the last page.
|
|
11
|
+
# @!attribute [r] error
|
|
12
|
+
# @return [String, nil] error message when the query itself is invalid
|
|
13
|
+
SearchResult = Struct.new(:data, :next_page, :error, keyword_init: true)
|
|
14
|
+
|
|
15
|
+
# Result from POST /v3/filter.
|
|
16
|
+
#
|
|
17
|
+
# Like {SearchResult} but results are sorted alphabetically by FIGI and a total count is included.
|
|
18
|
+
#
|
|
19
|
+
# @!attribute [r] data
|
|
20
|
+
# @return [Array<FigiResult>] instruments matching the filter for this page
|
|
21
|
+
# @!attribute [r] next_page
|
|
22
|
+
# @return [String, nil] opaque pagination token; pass as +start:+ to {Client#filter} to fetch
|
|
23
|
+
# the next page. nil when this is the last page.
|
|
24
|
+
# @!attribute [r] total
|
|
25
|
+
# @return [Integer] total number of matching instruments across all pages
|
|
26
|
+
# @!attribute [r] error
|
|
27
|
+
# @return [String, nil] error message when the request is invalid
|
|
28
|
+
FilterResult = Struct.new(:data, :next_page, :total, :error, keyword_init: true)
|
|
29
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "openfigi_ruby/version"
|
|
4
|
+
require_relative "openfigi_ruby/configuration"
|
|
5
|
+
require_relative "openfigi_ruby/error"
|
|
6
|
+
require_relative "openfigi_ruby/figi_result"
|
|
7
|
+
require_relative "openfigi_ruby/mapping_result"
|
|
8
|
+
require_relative "openfigi_ruby/search_result"
|
|
9
|
+
require_relative "openfigi_ruby/client"
|
|
10
|
+
|
|
11
|
+
# Ruby client for the OpenFIGI V3 API.
|
|
12
|
+
#
|
|
13
|
+
# Provides identifier mapping, keyword search, and filtering for Financial
|
|
14
|
+
# Instrument Global Identifiers (FIGIs).
|
|
15
|
+
#
|
|
16
|
+
# @example Configure globally and create a client
|
|
17
|
+
# OpenfigiRuby.configure do |config|
|
|
18
|
+
# config.api_key = ENV["OPENFIGI_API_KEY"]
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# client = OpenfigiRuby::Client.new
|
|
22
|
+
# results = client.mapping([{ id_type: "ID_ISIN", id_value: "US0378331005" }])
|
|
23
|
+
module OpenfigiRuby
|
|
24
|
+
class << self
|
|
25
|
+
# Returns the global {Configuration} instance.
|
|
26
|
+
# @return [Configuration]
|
|
27
|
+
def configuration
|
|
28
|
+
@configuration ||= Configuration.new
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Configures the gem globally.
|
|
32
|
+
#
|
|
33
|
+
# @example
|
|
34
|
+
# OpenfigiRuby.configure do |config|
|
|
35
|
+
# config.api_key = ENV["OPENFIGI_API_KEY"]
|
|
36
|
+
# end
|
|
37
|
+
# @yieldparam config [Configuration]
|
|
38
|
+
# @return [void]
|
|
39
|
+
def configure
|
|
40
|
+
yield configuration
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: openfigi_ruby
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Phong Si
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-05-13 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: minitest
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '5.0'
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '5.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: webmock
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '3.0'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '3.0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: yard
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0.9'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '0.9'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: webrick
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '1.0'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '1.0'
|
|
69
|
+
description: Maps financial identifiers (ISIN, CUSIP, ticker, etc.) to FIGIs via the
|
|
70
|
+
OpenFIGI V3 API. Supports bulk mapping, keyword search, and filtering.
|
|
71
|
+
email: []
|
|
72
|
+
executables: []
|
|
73
|
+
extensions: []
|
|
74
|
+
extra_rdoc_files: []
|
|
75
|
+
files:
|
|
76
|
+
- ".yardopts"
|
|
77
|
+
- CLAUDE.md
|
|
78
|
+
- LICENSE.txt
|
|
79
|
+
- README.md
|
|
80
|
+
- Rakefile
|
|
81
|
+
- lib/openfigi_ruby.rb
|
|
82
|
+
- lib/openfigi_ruby/client.rb
|
|
83
|
+
- lib/openfigi_ruby/configuration.rb
|
|
84
|
+
- lib/openfigi_ruby/error.rb
|
|
85
|
+
- lib/openfigi_ruby/figi_result.rb
|
|
86
|
+
- lib/openfigi_ruby/mapping_result.rb
|
|
87
|
+
- lib/openfigi_ruby/search_result.rb
|
|
88
|
+
- lib/openfigi_ruby/version.rb
|
|
89
|
+
- sig/openfigi_ruby.rbs
|
|
90
|
+
homepage: https://github.com/phongsi/openfigi_ruby
|
|
91
|
+
licenses:
|
|
92
|
+
- MIT
|
|
93
|
+
metadata:
|
|
94
|
+
homepage_uri: https://github.com/phongsi/openfigi_ruby
|
|
95
|
+
source_code_uri: https://github.com/phongsi/openfigi_ruby
|
|
96
|
+
post_install_message:
|
|
97
|
+
rdoc_options: []
|
|
98
|
+
require_paths:
|
|
99
|
+
- lib
|
|
100
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
101
|
+
requirements:
|
|
102
|
+
- - ">="
|
|
103
|
+
- !ruby/object:Gem::Version
|
|
104
|
+
version: 3.1.0
|
|
105
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - ">="
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '0'
|
|
110
|
+
requirements: []
|
|
111
|
+
rubygems_version: 3.4.19
|
|
112
|
+
signing_key:
|
|
113
|
+
specification_version: 4
|
|
114
|
+
summary: Ruby client for the OpenFIGI V3 API
|
|
115
|
+
test_files: []
|