sec_id 4.4.1 → 5.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 +4 -4
- data/CHANGELOG.md +59 -3
- data/MIGRATION.md +257 -0
- data/README.md +286 -148
- data/lib/sec_id/base.rb +43 -29
- data/lib/sec_id/cei.rb +14 -2
- data/lib/sec_id/cfi.rb +41 -12
- data/lib/sec_id/cik.rb +21 -18
- data/lib/sec_id/concerns/checkable.rb +56 -14
- data/lib/sec_id/concerns/identifier_metadata.rb +56 -0
- data/lib/sec_id/concerns/normalizable.rb +60 -16
- data/lib/sec_id/concerns/validatable.rb +158 -0
- data/lib/sec_id/cusip.rb +24 -8
- data/lib/sec_id/detector.rb +156 -0
- data/lib/sec_id/errors.rb +67 -0
- data/lib/sec_id/figi.rb +38 -8
- data/lib/sec_id/fisn.rb +15 -4
- data/lib/sec_id/iban/country_rules.rb +4 -2
- data/lib/sec_id/iban.rb +59 -12
- data/lib/sec_id/isin.rb +25 -6
- data/lib/sec_id/lei.rb +22 -9
- data/lib/sec_id/occ.rb +50 -25
- data/lib/sec_id/sedol.rb +12 -7
- data/lib/sec_id/valoren.rb +29 -22
- data/lib/sec_id/version.rb +2 -2
- data/lib/sec_id/wkn.rb +10 -7
- data/lib/sec_id.rb +127 -6
- data/sec_id.gemspec +6 -3
- metadata +11 -3
data/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# SecID [](https://rubygems.org/gems/sec_id) [](https://app.codecov.io/gh/svyatov/sec_id) [](https://github.com/svyatov/sec_id/actions?query=workflow%3ACI)
|
|
2
2
|
|
|
3
3
|
> Validate securities identification numbers with ease!
|
|
4
4
|
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
- [Supported Ruby Versions](#supported-ruby-versions)
|
|
8
8
|
- [Installation](#installation)
|
|
9
9
|
- [Supported Standards and Usage](#supported-standards-and-usage)
|
|
10
|
+
- [Metadata Registry](#metadata-registry) - enumerate, filter, look up, and detect identifier types
|
|
11
|
+
- [Structured Validation](#structured-validation) - detailed error codes and messages
|
|
10
12
|
- [ISIN](#isin) - International Securities Identification Number
|
|
11
13
|
- [CUSIP](#cusip) - Committee on Uniform Securities Identification Procedures
|
|
12
14
|
- [CEI](#cei) - CUSIP Entity Identifier
|
|
@@ -20,6 +22,7 @@
|
|
|
20
22
|
- [Valoren](#valoren) - Swiss Security Number
|
|
21
23
|
- [CFI](#cfi) - Classification of Financial Instruments
|
|
22
24
|
- [FISN](#fisn) - Financial Instrument Short Name
|
|
25
|
+
- [Lookup Service Integration](#lookup-service-integration)
|
|
23
26
|
- [Development](#development)
|
|
24
27
|
- [Contributing](#contributing)
|
|
25
28
|
- [Changelog](#changelog)
|
|
@@ -28,14 +31,14 @@
|
|
|
28
31
|
|
|
29
32
|
## Supported Ruby Versions
|
|
30
33
|
|
|
31
|
-
Ruby 3.
|
|
34
|
+
Ruby 3.2+ is required.
|
|
32
35
|
|
|
33
36
|
## Installation
|
|
34
37
|
|
|
35
38
|
Add this line to your application's Gemfile:
|
|
36
39
|
|
|
37
40
|
```ruby
|
|
38
|
-
gem 'sec_id', '~>
|
|
41
|
+
gem 'sec_id', '~> 5.1'
|
|
39
42
|
```
|
|
40
43
|
|
|
41
44
|
And then execute:
|
|
@@ -50,16 +53,152 @@ Or install it yourself:
|
|
|
50
53
|
gem install sec_id
|
|
51
54
|
```
|
|
52
55
|
|
|
56
|
+
**Upgrading from v4?** See [MIGRATION.md](MIGRATION.md) for a step-by-step guide.
|
|
57
|
+
|
|
53
58
|
## Supported Standards and Usage
|
|
54
59
|
|
|
55
|
-
All identifier classes provide `valid
|
|
60
|
+
All identifier classes provide `valid?`, `errors`, `validate`, `validate!` methods at both class and instance levels.
|
|
61
|
+
|
|
62
|
+
**All identifiers** support normalization and display formatting:
|
|
63
|
+
- `.normalize(id)` - strips separators, upcases, validates, and returns the canonical string
|
|
64
|
+
- `#normalized` / `#normalize` - returns the canonical string for a valid instance
|
|
65
|
+
- `#normalize!` - mutates `full_id` to canonical form, returns `self`
|
|
66
|
+
- `#to_pretty_s` / `.to_pretty_s(id)` - returns a human-readable formatted string, or `nil` for invalid input
|
|
67
|
+
|
|
68
|
+
**All identifiers** support hash serialization:
|
|
69
|
+
- `#to_h` - returns a hash with `:type`, `:full_id`, `:normalized`, `:valid`, and `:components` keys
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
SecID::ISIN.new('US5949181045').to_h
|
|
73
|
+
# => { type: :isin, full_id: 'US5949181045', normalized: 'US5949181045',
|
|
74
|
+
# valid: true, components: { country_code: 'US', nsin: '594918104', check_digit: 5 } }
|
|
75
|
+
|
|
76
|
+
SecID::ISIN.new('INVALID').to_h
|
|
77
|
+
# => { type: :isin, full_id: 'INVALID', normalized: nil,
|
|
78
|
+
# valid: false, components: { country_code: nil, nsin: nil, check_digit: nil } }
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**All identifiers** support value equality — two instances of the same type with the same normalized form are equal:
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
a = SecID::ISIN.new('US5949181045')
|
|
85
|
+
b = SecID::ISIN.new('us 5949 1810 45')
|
|
86
|
+
|
|
87
|
+
a == b # => true
|
|
88
|
+
a.eql?(b) # => true
|
|
89
|
+
|
|
90
|
+
# Works as Hash keys and in Sets
|
|
91
|
+
{ a => 'MSFT' }[b] # => 'MSFT'
|
|
92
|
+
Set.new([a, b]).size # => 1
|
|
93
|
+
```
|
|
56
94
|
|
|
57
95
|
**Check-digit based identifiers** (ISIN, CUSIP, CEI, SEDOL, FIGI, LEI, IBAN) also provide:
|
|
58
|
-
- `restore
|
|
96
|
+
- `restore` / `.restore` - returns the full identifier string with correct check-digit (no mutation)
|
|
97
|
+
- `restore!` / `.restore!` - restores check-digit in place and returns `self` / instance
|
|
59
98
|
- `check_digit` / `calculate_check_digit` - calculates and returns the check-digit
|
|
60
99
|
|
|
61
|
-
|
|
62
|
-
|
|
100
|
+
### Metadata Registry
|
|
101
|
+
|
|
102
|
+
All identifier classes are registered automatically and can be enumerated, filtered, and looked up by symbol key:
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
# Look up by symbol key
|
|
106
|
+
SecID[:isin] # => SecID::ISIN
|
|
107
|
+
SecID[:cusip] # => SecID::CUSIP
|
|
108
|
+
|
|
109
|
+
# Enumerate all identifier classes
|
|
110
|
+
SecID.identifiers # => [SecID::ISIN, SecID::CUSIP, ...]
|
|
111
|
+
SecID.identifiers.map(&:short_name) # => ["ISIN", "CUSIP", "SEDOL", ...]
|
|
112
|
+
|
|
113
|
+
# Query metadata
|
|
114
|
+
SecID::ISIN.short_name # => "ISIN"
|
|
115
|
+
SecID::ISIN.full_name # => "International Securities Identification Number"
|
|
116
|
+
SecID::ISIN.id_length # => 12
|
|
117
|
+
SecID::ISIN.example # => "US5949181045"
|
|
118
|
+
SecID::ISIN.has_check_digit? # => true
|
|
119
|
+
|
|
120
|
+
# Filter with standard Ruby
|
|
121
|
+
SecID.identifiers.select(&:has_check_digit?).map(&:short_name)
|
|
122
|
+
# => ["ISIN", "CUSIP", "SEDOL", "FIGI", "LEI", "IBAN", "CEI"]
|
|
123
|
+
|
|
124
|
+
# Detect identifier type from an unknown string
|
|
125
|
+
# Results are sorted by specificity: check-digit types first, then by length precision
|
|
126
|
+
SecID.detect('US5949181045') # => [:isin]
|
|
127
|
+
SecID.detect('037833100') # => [:cusip, :valoren, :cik]
|
|
128
|
+
SecID.detect('APPLE INC/SH') # => [:fisn]
|
|
129
|
+
SecID.detect('INVALID') # => []
|
|
130
|
+
|
|
131
|
+
# Quick boolean validation
|
|
132
|
+
SecID.valid?('US5949181045') # => true (any type)
|
|
133
|
+
SecID.valid?('INVALID') # => false
|
|
134
|
+
SecID.valid?('US5949181045', types: [:isin]) # => true
|
|
135
|
+
SecID.valid?('594918104', types: %i[cusip sedol]) # => true
|
|
136
|
+
SecID.valid?('US5949181045', types: [:cusip]) # => false
|
|
137
|
+
|
|
138
|
+
# Parse into a typed instance (returns the most specific match)
|
|
139
|
+
SecID.parse('US5949181045') # => #<SecID::ISIN>
|
|
140
|
+
SecID.parse('594918104') # => #<SecID::CUSIP>
|
|
141
|
+
SecID.parse('unknown') # => nil
|
|
142
|
+
SecID.parse('594918104', types: [:cusip]) # => #<SecID::CUSIP>
|
|
143
|
+
|
|
144
|
+
# Bang version raises on failure
|
|
145
|
+
SecID.parse!('US5949181045') # => #<SecID::ISIN>
|
|
146
|
+
SecID.parse!('unknown') # raises SecID::InvalidFormatError
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Structured Validation
|
|
150
|
+
|
|
151
|
+
All identifier classes provide a Rails-like `#errors` API for detailed error reporting:
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
isin = SecID::ISIN.new('US5949181040')
|
|
155
|
+
isin.errors.none? # => false
|
|
156
|
+
isin.errors.messages # => ["Check digit '0' is invalid, expected '5'"]
|
|
157
|
+
isin.errors.details # => [{ error: :invalid_check_digit, message: "Check digit '0' is invalid, expected '5'" }]
|
|
158
|
+
isin.errors.any? # => true
|
|
159
|
+
isin.errors.empty? # => false
|
|
160
|
+
isin.errors.size # => 1
|
|
161
|
+
isin.errors.to_a # => same as messages
|
|
162
|
+
|
|
163
|
+
# Class-level convenience method (returns the instance with errors cached)
|
|
164
|
+
SecID::ISIN.validate('US5949181040') # => #<SecID::ISIN>
|
|
165
|
+
SecID::ISIN.validate('US5949181040').errors # => #<SecID::Errors>
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**Common error codes** (all identifier types):
|
|
169
|
+
- `:invalid_length` - wrong number of characters
|
|
170
|
+
- `:invalid_characters` - contains characters not allowed for this type
|
|
171
|
+
- `:invalid_format` - correct length and characters but wrong structure
|
|
172
|
+
|
|
173
|
+
**Type-specific error codes:**
|
|
174
|
+
- `:invalid_check_digit` - check digit mismatch (ISIN, CUSIP, SEDOL, FIGI, LEI, IBAN, CEI)
|
|
175
|
+
- `:invalid_prefix` - restricted FIGI prefix (FIGI)
|
|
176
|
+
- `:invalid_category` - unknown CFI category code (CFI)
|
|
177
|
+
- `:invalid_group` - unknown CFI group code for category (CFI)
|
|
178
|
+
- `:invalid_bban` - BBAN format invalid for country (IBAN)
|
|
179
|
+
- `:invalid_date` - unparseable expiration date (OCC)
|
|
180
|
+
|
|
181
|
+
#### Fail-fast validation with `validate!`
|
|
182
|
+
|
|
183
|
+
Use `validate!` when you want to raise an exception on invalid input instead of inspecting errors:
|
|
184
|
+
|
|
185
|
+
```ruby
|
|
186
|
+
# Returns self when valid (enables chaining)
|
|
187
|
+
SecID::ISIN.new('US5949181045').validate! # => #<SecID::ISIN>
|
|
188
|
+
|
|
189
|
+
# Raises with a descriptive message when invalid
|
|
190
|
+
SecID::ISIN.new('INVALID').validate!
|
|
191
|
+
# => SecID::InvalidFormatError: Expected 12 characters, got 7
|
|
192
|
+
|
|
193
|
+
SecID::ISIN.new('US5949181040').validate!
|
|
194
|
+
# => SecID::InvalidCheckDigitError: Check digit '0' is invalid, expected '5'
|
|
195
|
+
|
|
196
|
+
SecID::FIGI.new('BSG000BLNNH6').validate!
|
|
197
|
+
# => SecID::InvalidStructureError: Prefix 'BS' is restricted
|
|
198
|
+
|
|
199
|
+
# Class-level convenience method (returns the instance)
|
|
200
|
+
isin = SecID::ISIN.validate!('US5949181045') # => #<SecID::ISIN>
|
|
201
|
+
```
|
|
63
202
|
|
|
64
203
|
### ISIN
|
|
65
204
|
|
|
@@ -67,42 +206,43 @@ All identifier classes provide `valid?` and `valid_format?` methods at both clas
|
|
|
67
206
|
|
|
68
207
|
```ruby
|
|
69
208
|
# class level
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
209
|
+
SecID::ISIN.valid?('US5949181045') # => true
|
|
210
|
+
SecID::ISIN.restore('US594918104') # => 'US5949181045'
|
|
211
|
+
SecID::ISIN.restore!('US594918104') # => #<SecID::ISIN>
|
|
212
|
+
SecID::ISIN.check_digit('US594918104') # => 5
|
|
74
213
|
|
|
75
214
|
# instance level
|
|
76
|
-
isin =
|
|
77
|
-
isin.
|
|
215
|
+
isin = SecID::ISIN.new('US5949181045')
|
|
216
|
+
isin.full_id # => 'US5949181045'
|
|
78
217
|
isin.country_code # => 'US'
|
|
79
218
|
isin.nsin # => '594918104'
|
|
80
219
|
isin.check_digit # => 5
|
|
81
220
|
isin.valid? # => true
|
|
82
|
-
isin.
|
|
83
|
-
isin.restore! # =>
|
|
221
|
+
isin.restore # => 'US5949181045'
|
|
222
|
+
isin.restore! # => #<SecID::ISIN> (mutates instance)
|
|
84
223
|
isin.calculate_check_digit # => 5
|
|
85
|
-
isin.
|
|
224
|
+
isin.to_pretty_s # => 'US 594918104 5'
|
|
225
|
+
isin.to_cusip # => #<SecID::CUSIP>
|
|
86
226
|
isin.nsin_type # => :cusip
|
|
87
|
-
isin.to_nsin # => #<
|
|
227
|
+
isin.to_nsin # => #<SecID::CUSIP>
|
|
88
228
|
|
|
89
229
|
# NSIN extraction for different countries
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
230
|
+
SecID::ISIN.new('GB00B02H2F76').nsin_type # => :sedol
|
|
231
|
+
SecID::ISIN.new('GB00B02H2F76').to_nsin # => #<SecID::SEDOL>
|
|
232
|
+
SecID::ISIN.new('DE0007164600').nsin_type # => :wkn
|
|
233
|
+
SecID::ISIN.new('DE0007164600').to_nsin # => #<SecID::WKN>
|
|
234
|
+
SecID::ISIN.new('CH0012221716').nsin_type # => :valoren
|
|
235
|
+
SecID::ISIN.new('CH0012221716').to_nsin # => #<SecID::Valoren>
|
|
236
|
+
SecID::ISIN.new('FR0000120271').nsin_type # => :generic
|
|
237
|
+
SecID::ISIN.new('FR0000120271').to_nsin # => '000012027' (raw NSIN string)
|
|
98
238
|
|
|
99
239
|
# Type-specific conversion methods with validation
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
240
|
+
SecID::ISIN.new('GB00B02H2F76').sedol? # => true
|
|
241
|
+
SecID::ISIN.new('GB00B02H2F76').to_sedol # => #<SecID::SEDOL>
|
|
242
|
+
SecID::ISIN.new('DE0007164600').wkn? # => true
|
|
243
|
+
SecID::ISIN.new('DE0007164600').to_wkn # => #<SecID::WKN>
|
|
244
|
+
SecID::ISIN.new('CH0012221716').valoren? # => true
|
|
245
|
+
SecID::ISIN.new('CH0012221716').to_valoren # => #<SecID::Valoren>
|
|
106
246
|
```
|
|
107
247
|
|
|
108
248
|
### CUSIP
|
|
@@ -111,22 +251,23 @@ SecId::ISIN.new('CH0012221716').to_valoren # => #<SecId::Valoren>
|
|
|
111
251
|
|
|
112
252
|
```ruby
|
|
113
253
|
# class level
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
254
|
+
SecID::CUSIP.valid?('594918104') # => true
|
|
255
|
+
SecID::CUSIP.restore('59491810') # => '594918104'
|
|
256
|
+
SecID::CUSIP.restore!('59491810') # => #<SecID::CUSIP>
|
|
257
|
+
SecID::CUSIP.check_digit('59491810') # => 4
|
|
118
258
|
|
|
119
259
|
# instance level
|
|
120
|
-
cusip =
|
|
121
|
-
cusip.
|
|
260
|
+
cusip = SecID::CUSIP.new('594918104')
|
|
261
|
+
cusip.full_id # => '594918104'
|
|
122
262
|
cusip.cusip6 # => '594918'
|
|
123
263
|
cusip.issue # => '10'
|
|
124
264
|
cusip.check_digit # => 4
|
|
125
265
|
cusip.valid? # => true
|
|
126
|
-
cusip.
|
|
127
|
-
cusip.restore! # =>
|
|
266
|
+
cusip.restore # => '594918104'
|
|
267
|
+
cusip.restore! # => #<SecID::CUSIP> (mutates instance)
|
|
128
268
|
cusip.calculate_check_digit # => 4
|
|
129
|
-
cusip.
|
|
269
|
+
cusip.to_pretty_s # => '594918 10 4'
|
|
270
|
+
cusip.to_isin('US') # => #<SecID::ISIN>
|
|
130
271
|
cusip.cins? # => false
|
|
131
272
|
```
|
|
132
273
|
|
|
@@ -136,21 +277,21 @@ cusip.cins? # => false
|
|
|
136
277
|
|
|
137
278
|
```ruby
|
|
138
279
|
# class level
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
280
|
+
SecID::CEI.valid?('A0BCDEFGH1') # => true
|
|
281
|
+
SecID::CEI.restore('A0BCDEFGH') # => 'A0BCDEFGH1'
|
|
282
|
+
SecID::CEI.restore!('A0BCDEFGH') # => #<SecID::CEI>
|
|
283
|
+
SecID::CEI.check_digit('A0BCDEFGH') # => 1
|
|
143
284
|
|
|
144
285
|
# instance level
|
|
145
|
-
cei =
|
|
146
|
-
cei.
|
|
286
|
+
cei = SecID::CEI.new('A0BCDEFGH1')
|
|
287
|
+
cei.full_id # => 'A0BCDEFGH1'
|
|
147
288
|
cei.prefix # => 'A'
|
|
148
289
|
cei.numeric # => '0'
|
|
149
290
|
cei.entity_id # => 'BCDEFGH'
|
|
150
291
|
cei.check_digit # => 1
|
|
151
292
|
cei.valid? # => true
|
|
152
|
-
cei.
|
|
153
|
-
cei.restore! # =>
|
|
293
|
+
cei.restore # => 'A0BCDEFGH1'
|
|
294
|
+
cei.restore! # => #<SecID::CEI> (mutates instance)
|
|
154
295
|
cei.calculate_check_digit # => 1
|
|
155
296
|
```
|
|
156
297
|
|
|
@@ -160,21 +301,21 @@ cei.calculate_check_digit # => 1
|
|
|
160
301
|
|
|
161
302
|
```ruby
|
|
162
303
|
# class level
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
304
|
+
SecID::SEDOL.valid?('B0Z52W5') # => true
|
|
305
|
+
SecID::SEDOL.restore('B0Z52W') # => 'B0Z52W5'
|
|
306
|
+
SecID::SEDOL.restore!('B0Z52W') # => #<SecID::SEDOL>
|
|
307
|
+
SecID::SEDOL.check_digit('B0Z52W') # => 5
|
|
167
308
|
|
|
168
309
|
# instance level
|
|
169
|
-
sedol =
|
|
170
|
-
sedol.
|
|
310
|
+
sedol = SecID::SEDOL.new('B0Z52W5')
|
|
311
|
+
sedol.full_id # => 'B0Z52W5'
|
|
171
312
|
sedol.check_digit # => 5
|
|
172
313
|
sedol.valid? # => true
|
|
173
|
-
sedol.
|
|
174
|
-
sedol.restore! # =>
|
|
314
|
+
sedol.restore # => 'B0Z52W5'
|
|
315
|
+
sedol.restore! # => #<SecID::SEDOL> (mutates instance)
|
|
175
316
|
sedol.calculate_check_digit # => 5
|
|
176
|
-
sedol.to_isin # => #<
|
|
177
|
-
sedol.to_isin('IE') # => #<
|
|
317
|
+
sedol.to_isin # => #<SecID::ISIN> (GB ISIN by default)
|
|
318
|
+
sedol.to_isin('IE') # => #<SecID::ISIN> (IE ISIN)
|
|
178
319
|
```
|
|
179
320
|
|
|
180
321
|
### FIGI
|
|
@@ -183,21 +324,22 @@ sedol.to_isin('IE') # => #<SecId::ISIN> (IE ISIN)
|
|
|
183
324
|
|
|
184
325
|
```ruby
|
|
185
326
|
# class level
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
327
|
+
SecID::FIGI.valid?('BBG000DMBXR2') # => true
|
|
328
|
+
SecID::FIGI.restore('BBG000DMBXR') # => 'BBG000DMBXR2'
|
|
329
|
+
SecID::FIGI.restore!('BBG000DMBXR') # => #<SecID::FIGI>
|
|
330
|
+
SecID::FIGI.check_digit('BBG000DMBXR') # => 2
|
|
190
331
|
|
|
191
332
|
# instance level
|
|
192
|
-
figi =
|
|
193
|
-
figi.
|
|
333
|
+
figi = SecID::FIGI.new('BBG000DMBXR2')
|
|
334
|
+
figi.full_id # => 'BBG000DMBXR2'
|
|
194
335
|
figi.prefix # => 'BB'
|
|
195
336
|
figi.random_part # => '000DMBXR'
|
|
196
337
|
figi.check_digit # => 2
|
|
197
338
|
figi.valid? # => true
|
|
198
|
-
figi.
|
|
199
|
-
figi.restore! # =>
|
|
339
|
+
figi.restore # => 'BBG000DMBXR2'
|
|
340
|
+
figi.restore! # => #<SecID::FIGI> (mutates instance)
|
|
200
341
|
figi.calculate_check_digit # => 2
|
|
342
|
+
figi.to_pretty_s # => 'BBG 000DMBXR 2'
|
|
201
343
|
```
|
|
202
344
|
|
|
203
345
|
### LEI
|
|
@@ -206,22 +348,23 @@ figi.calculate_check_digit # => 2
|
|
|
206
348
|
|
|
207
349
|
```ruby
|
|
208
350
|
# class level
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
351
|
+
SecID::LEI.valid?('5493006MHB84DD0ZWV18') # => true
|
|
352
|
+
SecID::LEI.restore('5493006MHB84DD0ZWV') # => '5493006MHB84DD0ZWV18'
|
|
353
|
+
SecID::LEI.restore!('5493006MHB84DD0ZWV') # => #<SecID::LEI>
|
|
354
|
+
SecID::LEI.check_digit('5493006MHB84DD0ZWV') # => 18
|
|
213
355
|
|
|
214
356
|
# instance level
|
|
215
|
-
lei =
|
|
216
|
-
lei.
|
|
357
|
+
lei = SecID::LEI.new('5493006MHB84DD0ZWV18')
|
|
358
|
+
lei.full_id # => '5493006MHB84DD0ZWV18'
|
|
217
359
|
lei.lou_id # => '5493'
|
|
218
360
|
lei.reserved # => '00'
|
|
219
361
|
lei.entity_id # => '6MHB84DD0ZWV'
|
|
220
362
|
lei.check_digit # => 18
|
|
221
363
|
lei.valid? # => true
|
|
222
|
-
lei.
|
|
223
|
-
lei.restore! # =>
|
|
364
|
+
lei.restore # => '5493006MHB84DD0ZWV18'
|
|
365
|
+
lei.restore! # => #<SecID::LEI> (mutates instance)
|
|
224
366
|
lei.calculate_check_digit # => 18
|
|
367
|
+
lei.to_pretty_s # => '5493 006M HB84 DD0Z WV18'
|
|
225
368
|
```
|
|
226
369
|
|
|
227
370
|
### IBAN
|
|
@@ -230,24 +373,25 @@ lei.calculate_check_digit # => 18
|
|
|
230
373
|
|
|
231
374
|
```ruby
|
|
232
375
|
# class level
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
376
|
+
SecID::IBAN.valid?('DE89370400440532013000') # => true
|
|
377
|
+
SecID::IBAN.restore('DE370400440532013000') # => 'DE89370400440532013000'
|
|
378
|
+
SecID::IBAN.restore!('DE370400440532013000') # => #<SecID::IBAN>
|
|
379
|
+
SecID::IBAN.check_digit('DE370400440532013000') # => 89
|
|
237
380
|
|
|
238
381
|
# instance level
|
|
239
|
-
iban =
|
|
240
|
-
iban.
|
|
382
|
+
iban = SecID::IBAN.new('DE89370400440532013000')
|
|
383
|
+
iban.full_id # => 'DE89370400440532013000'
|
|
241
384
|
iban.country_code # => 'DE'
|
|
242
385
|
iban.bban # => '370400440532013000'
|
|
243
386
|
iban.bank_code # => '37040044'
|
|
244
387
|
iban.account_number # => '0532013000'
|
|
245
388
|
iban.check_digit # => 89
|
|
246
389
|
iban.valid? # => true
|
|
247
|
-
iban.
|
|
248
|
-
iban.restore! # =>
|
|
390
|
+
iban.restore # => 'DE89370400440532013000'
|
|
391
|
+
iban.restore! # => #<SecID::IBAN> (mutates instance)
|
|
249
392
|
iban.calculate_check_digit # => 89
|
|
250
393
|
iban.known_country? # => true
|
|
394
|
+
iban.to_pretty_s # => 'DE89 3704 0044 0532 0130 00'
|
|
251
395
|
```
|
|
252
396
|
|
|
253
397
|
Full BBAN structural validation is supported for EU/EEA countries. Other countries have length-only validation.
|
|
@@ -258,58 +402,51 @@ Full BBAN structural validation is supported for EU/EEA countries. Other countri
|
|
|
258
402
|
|
|
259
403
|
```ruby
|
|
260
404
|
# class level
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
SecId::CIK.normalize!('1094517') # => '0001094517'
|
|
405
|
+
SecID::CIK.valid?('0001094517') # => true
|
|
406
|
+
SecID::CIK.normalize('1094517') # => '0001094517'
|
|
264
407
|
|
|
265
408
|
# instance level
|
|
266
|
-
cik =
|
|
267
|
-
cik.
|
|
409
|
+
cik = SecID::CIK.new('0001094517')
|
|
410
|
+
cik.full_id # => '0001094517'
|
|
268
411
|
cik.padding # => '000'
|
|
269
412
|
cik.identifier # => '1094517'
|
|
270
413
|
cik.valid? # => true
|
|
271
|
-
cik.
|
|
272
|
-
cik.normalize! # =>
|
|
273
|
-
cik.to_s # => '0001094517'
|
|
414
|
+
cik.normalized # => '0001094517'
|
|
415
|
+
cik.normalize! # => #<SecID::CIK> (mutates full_id, returns self)
|
|
274
416
|
```
|
|
275
417
|
|
|
276
418
|
### OCC
|
|
277
419
|
|
|
278
|
-
> [Options Clearing Corporation Symbol](https://en.wikipedia.org/wiki/Option_symbol#The_OCC_Option_Symbol) - a 21-character code used to identify equity options contracts.
|
|
420
|
+
> [Options Clearing Corporation Symbol](https://en.wikipedia.org/wiki/Option_symbol#The_OCC_Option_Symbol) - a 16-to-21-character code used to identify equity options contracts.
|
|
279
421
|
|
|
280
422
|
```ruby
|
|
281
423
|
# class level
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
SecId::OCC.build(
|
|
424
|
+
SecID::OCC.valid?('BRKB 100417C00090000') # => true
|
|
425
|
+
SecID::OCC.normalize('BRKB100417C00090000') # => 'BRKB 100417C00090000'
|
|
426
|
+
SecID::OCC.build(
|
|
286
427
|
underlying: 'BRKB',
|
|
287
428
|
date: Date.new(2010, 4, 17),
|
|
288
429
|
type: 'C',
|
|
289
430
|
strike: 90,
|
|
290
|
-
) # => #<
|
|
431
|
+
) # => #<SecID::OCC>
|
|
291
432
|
|
|
292
433
|
# instance level
|
|
293
|
-
occ =
|
|
294
|
-
occ.
|
|
434
|
+
occ = SecID::OCC.new('BRKB 100417C00090000')
|
|
435
|
+
occ.full_id # => 'BRKB 100417C00090000'
|
|
295
436
|
occ.underlying # => 'BRKB'
|
|
296
437
|
occ.date_str # => '100417'
|
|
297
438
|
occ.date_obj # => #<Date: 2010-04-17>
|
|
298
439
|
occ.type # => 'C'
|
|
299
440
|
occ.strike # => 90.0
|
|
300
441
|
occ.valid? # => true
|
|
301
|
-
occ.
|
|
302
|
-
occ.normalize! # => 'BRKB 100417C00090000'
|
|
303
|
-
|
|
304
|
-
occ = SecId::OCC.new('BRKB 2010-04-17C00090000')
|
|
305
|
-
occ.valid_format? # => false
|
|
306
|
-
occ.normalize! # raises SecId::InvalidFormatError
|
|
442
|
+
occ.normalized # => 'BRKB 100417C00090000'
|
|
307
443
|
|
|
308
|
-
occ =
|
|
309
|
-
occ.
|
|
444
|
+
occ = SecID::OCC.new('X 250620C00050000')
|
|
445
|
+
occ.full_id # => 'X 250620C00050000'
|
|
310
446
|
occ.valid? # => true
|
|
311
|
-
occ.normalize! # =>
|
|
312
|
-
occ.
|
|
447
|
+
occ.normalize! # => #<SecID::OCC> (mutates full_id, returns self)
|
|
448
|
+
occ.full_id # => 'X 250620C00050000'
|
|
449
|
+
occ.to_pretty_s # => 'X 250620 C 00050000'
|
|
313
450
|
```
|
|
314
451
|
|
|
315
452
|
### WKN
|
|
@@ -318,18 +455,16 @@ occ.full_symbol # => 'X 250620C00050000'
|
|
|
318
455
|
|
|
319
456
|
```ruby
|
|
320
457
|
# class level
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
SecId::WKN.valid_format?('514000') # => true
|
|
458
|
+
SecID::WKN.valid?('514000') # => true
|
|
459
|
+
SecID::WKN.valid?('CBK100') # => true
|
|
324
460
|
|
|
325
461
|
# instance level
|
|
326
|
-
wkn =
|
|
327
|
-
wkn.
|
|
462
|
+
wkn = SecID::WKN.new('514000')
|
|
463
|
+
wkn.full_id # => '514000'
|
|
328
464
|
wkn.identifier # => '514000'
|
|
329
465
|
wkn.valid? # => true
|
|
330
|
-
wkn.valid_format? # => true
|
|
331
466
|
wkn.to_s # => '514000'
|
|
332
|
-
wkn.to_isin # => #<
|
|
467
|
+
wkn.to_isin # => #<SecID::ISIN> (DE ISIN)
|
|
333
468
|
```
|
|
334
469
|
|
|
335
470
|
WKN excludes letters I and O to avoid confusion with digits 1 and 0.
|
|
@@ -340,24 +475,23 @@ WKN excludes letters I and O to avoid confusion with digits 1 and 0.
|
|
|
340
475
|
|
|
341
476
|
```ruby
|
|
342
477
|
# class level
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
SecId::Valoren.normalize!('3886335') # => '003886335'
|
|
478
|
+
SecID::Valoren.valid?('3886335') # => true
|
|
479
|
+
SecID::Valoren.valid?('24476758') # => true
|
|
480
|
+
SecID::Valoren.valid?('35514757') # => true
|
|
481
|
+
SecID::Valoren.valid?('97429325') # => true
|
|
482
|
+
SecID::Valoren.normalize('3886335') # => '003886335'
|
|
349
483
|
|
|
350
484
|
# instance level
|
|
351
|
-
valoren =
|
|
352
|
-
valoren.
|
|
485
|
+
valoren = SecID::Valoren.new('3886335')
|
|
486
|
+
valoren.full_id # => '3886335'
|
|
353
487
|
valoren.padding # => ''
|
|
354
488
|
valoren.identifier # => '3886335'
|
|
355
489
|
valoren.valid? # => true
|
|
356
|
-
valoren.
|
|
357
|
-
valoren.normalize! # =>
|
|
358
|
-
valoren.
|
|
359
|
-
valoren.to_isin # => #<
|
|
360
|
-
valoren.to_isin('LI') # => #<
|
|
490
|
+
valoren.normalized # => '003886335'
|
|
491
|
+
valoren.normalize! # => #<SecID::Valoren> (mutates full_id, returns self)
|
|
492
|
+
valoren.to_pretty_s # => '3 886 335'
|
|
493
|
+
valoren.to_isin # => #<SecID::ISIN> (CH ISIN by default)
|
|
494
|
+
valoren.to_isin('LI') # => #<SecID::ISIN> (LI ISIN)
|
|
361
495
|
```
|
|
362
496
|
|
|
363
497
|
### CFI
|
|
@@ -366,20 +500,17 @@ valoren.to_isin('LI') # => #<SecId::ISIN> (LI ISIN)
|
|
|
366
500
|
|
|
367
501
|
```ruby
|
|
368
502
|
# class level
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
SecId::CFI.valid_format?('ESXXXX') # => true
|
|
372
|
-
|
|
503
|
+
SecID::CFI.valid?('ESXXXX') # => true
|
|
504
|
+
SecID::CFI.valid?('ESVUFR') # => true
|
|
373
505
|
# instance level
|
|
374
|
-
cfi =
|
|
375
|
-
cfi.
|
|
506
|
+
cfi = SecID::CFI.new('ESVUFR')
|
|
507
|
+
cfi.full_id # => 'ESVUFR'
|
|
376
508
|
cfi.identifier # => 'ESVUFR'
|
|
377
509
|
cfi.category_code # => 'E'
|
|
378
510
|
cfi.group_code # => 'S'
|
|
379
511
|
cfi.category # => :equity
|
|
380
512
|
cfi.group # => :common_shares
|
|
381
513
|
cfi.valid? # => true
|
|
382
|
-
cfi.valid_format? # => true
|
|
383
514
|
|
|
384
515
|
# Equity-specific predicates
|
|
385
516
|
cfi.equity? # => true
|
|
@@ -397,23 +528,33 @@ CFI validates the category code (position 1) against 14 valid values and the gro
|
|
|
397
528
|
|
|
398
529
|
```ruby
|
|
399
530
|
# class level
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
SecId::FISN.valid_format?('APPLE INC/SH') # => true
|
|
403
|
-
|
|
531
|
+
SecID::FISN.valid?('APPLE INC/SH') # => true
|
|
532
|
+
SecID::FISN.valid?('apple inc/sh') # => true (normalized to uppercase)
|
|
404
533
|
# instance level
|
|
405
|
-
fisn =
|
|
406
|
-
fisn.
|
|
534
|
+
fisn = SecID::FISN.new('APPLE INC/SH')
|
|
535
|
+
fisn.full_id # => 'APPLE INC/SH'
|
|
407
536
|
fisn.identifier # => 'APPLE INC/SH'
|
|
408
537
|
fisn.issuer # => 'APPLE INC'
|
|
409
538
|
fisn.description # => 'SH'
|
|
410
539
|
fisn.valid? # => true
|
|
411
|
-
fisn.valid_format? # => true
|
|
412
540
|
fisn.to_s # => 'APPLE INC/SH'
|
|
413
541
|
```
|
|
414
542
|
|
|
415
543
|
FISN format: `Issuer Name/Abbreviated Instrument Description` with issuer (1-15 chars) and description (1-19 chars) separated by a forward slash. Character set: uppercase A-Z, digits 0-9, and space.
|
|
416
544
|
|
|
545
|
+
## Lookup Service Integration
|
|
546
|
+
|
|
547
|
+
SecID validates identifiers but does not include HTTP clients. The [`docs/guides/`](docs/guides/) directory provides integration patterns for external lookup services using only stdlib (`net/http`, `json`):
|
|
548
|
+
|
|
549
|
+
| Guide | Service | Identifier |
|
|
550
|
+
|-------|---------|------------|
|
|
551
|
+
| [OpenFIGI](docs/guides/openfigi.md) | [OpenFIGI API](https://www.openfigi.com/api) | FIGI |
|
|
552
|
+
| [SEC EDGAR](docs/guides/sec-edgar.md) | [SEC EDGAR](https://www.sec.gov/edgar/sec-api-documentation) | CIK |
|
|
553
|
+
| [GLEIF](docs/guides/gleif.md) | [GLEIF API](https://www.gleif.org/en/lei-data/gleif-api) | LEI |
|
|
554
|
+
| [Eurex](docs/guides/eurex.md) | [Eurex Reference Data](https://www.eurex.com/ex-en/data/free-reference-data-api) | ISIN |
|
|
555
|
+
|
|
556
|
+
Each guide includes a complete adapter class and a [runnable example](examples/).
|
|
557
|
+
|
|
417
558
|
## Development
|
|
418
559
|
|
|
419
560
|
After checking out the repo, run `bin/setup` to install dependencies.
|
|
@@ -424,12 +565,9 @@ To install this gem onto your local machine, run `bundle exec rake install`.
|
|
|
424
565
|
|
|
425
566
|
## Contributing
|
|
426
567
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
4. Commit using [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) format (`git commit -m 'feat: add some feature'`)
|
|
431
|
-
5. Push to the branch (`git push origin my-new-feature`)
|
|
432
|
-
6. Create a new Pull Request
|
|
568
|
+
Bug reports and pull requests are welcome on [GitHub](https://github.com/svyatov/sec_id). See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, code style, and PR guidelines.
|
|
569
|
+
|
|
570
|
+
This project follows the [Contributor Covenant Code of Conduct](CODE_OF_CONDUCT.md).
|
|
433
571
|
|
|
434
572
|
## Changelog
|
|
435
573
|
|