sec_id 4.2.0 → 4.3.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 +25 -0
- data/README.md +112 -92
- data/lib/sec_id/base.rb +118 -6
- data/lib/sec_id/cik.rb +33 -31
- data/lib/sec_id/cusip.rb +30 -8
- data/lib/sec_id/figi.rb +30 -6
- data/lib/sec_id/iban/country_rules.rb +266 -0
- data/lib/sec_id/iban.rb +158 -0
- data/lib/sec_id/isin.rb +33 -12
- data/lib/sec_id/lei.rb +67 -0
- data/lib/sec_id/normalizable.rb +35 -0
- data/lib/sec_id/occ.rb +111 -64
- data/lib/sec_id/sedol.rb +24 -6
- data/lib/sec_id/version.rb +1 -1
- data/lib/sec_id.rb +3 -0
- data/sec_id.gemspec +2 -1
- metadata +7 -3
data/lib/sec_id/lei.rb
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SecId
|
|
4
|
+
# Legal Entity Identifier (LEI) - a 20-character alphanumeric code that
|
|
5
|
+
# uniquely identifies legal entities participating in financial transactions.
|
|
6
|
+
#
|
|
7
|
+
# Format: 4-character LOU ID + 2-character reserved + 12-character entity ID + 2-digit check digit
|
|
8
|
+
#
|
|
9
|
+
# @see https://en.wikipedia.org/wiki/Legal_Entity_Identifier
|
|
10
|
+
# @see https://www.gleif.org/en/about-lei/iso-17442-the-lei-code-structure
|
|
11
|
+
#
|
|
12
|
+
# @example Validate a LEI
|
|
13
|
+
# SecId::LEI.valid?('529900T8BM49AURSDO55') #=> true
|
|
14
|
+
#
|
|
15
|
+
# @example Calculate check digit
|
|
16
|
+
# SecId::LEI.check_digit('529900T8BM49AURSDO') #=> 55
|
|
17
|
+
class LEI < Base
|
|
18
|
+
# Regular expression for parsing LEI components.
|
|
19
|
+
ID_REGEX = /\A
|
|
20
|
+
(?<identifier>
|
|
21
|
+
(?<lou_id>[0-9A-Z]{4})
|
|
22
|
+
(?<reserved>[0-9A-Z]{2})
|
|
23
|
+
(?<entity_id>[0-9A-Z]{12}))
|
|
24
|
+
(?<check_digit>\d{2})?
|
|
25
|
+
\z/x
|
|
26
|
+
|
|
27
|
+
# @return [String, nil] the 4-character Local Operating Unit (LOU) identifier
|
|
28
|
+
attr_reader :lou_id
|
|
29
|
+
|
|
30
|
+
# @return [String, nil] the 2-character reserved field (typically '00')
|
|
31
|
+
attr_reader :reserved
|
|
32
|
+
|
|
33
|
+
# @return [String, nil] the 12-character entity-specific identifier
|
|
34
|
+
attr_reader :entity_id
|
|
35
|
+
|
|
36
|
+
# @param lei [String] the LEI string to parse
|
|
37
|
+
def initialize(lei)
|
|
38
|
+
lei_parts = parse lei
|
|
39
|
+
@identifier = lei_parts[:identifier]
|
|
40
|
+
@lou_id = lei_parts[:lou_id]
|
|
41
|
+
@reserved = lei_parts[:reserved]
|
|
42
|
+
@entity_id = lei_parts[:entity_id]
|
|
43
|
+
@check_digit = lei_parts[:check_digit]&.to_i
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# @return [Integer] the calculated 2-digit check digit (1-98)
|
|
47
|
+
# @raise [InvalidFormatError] if the LEI format is invalid
|
|
48
|
+
def calculate_check_digit
|
|
49
|
+
validate_format_for_calculation!
|
|
50
|
+
mod97("#{numeric_identifier}00")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @return [String]
|
|
54
|
+
def to_s
|
|
55
|
+
return full_number unless check_digit
|
|
56
|
+
|
|
57
|
+
"#{identifier}#{check_digit.to_s.rjust(2, '0')}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
# @return [String] the numeric string representation
|
|
63
|
+
def numeric_identifier
|
|
64
|
+
identifier.each_char.map { |char| char_to_digit(char) }.join
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SecId
|
|
4
|
+
# Provides normalize! class method delegation for identifiers that support normalization.
|
|
5
|
+
# Include this module in classes that implement an instance-level normalize! method.
|
|
6
|
+
#
|
|
7
|
+
# @example
|
|
8
|
+
# class MyIdentifier < Base
|
|
9
|
+
# include Normalizable
|
|
10
|
+
#
|
|
11
|
+
# def normalize!
|
|
12
|
+
# # implementation
|
|
13
|
+
# end
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# MyIdentifier.normalize!('ABC123') #=> normalized string
|
|
17
|
+
module Normalizable
|
|
18
|
+
# @api private
|
|
19
|
+
def self.included(base)
|
|
20
|
+
base.extend(ClassMethods)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Class methods added when Normalizable is included.
|
|
24
|
+
module ClassMethods
|
|
25
|
+
# Normalizes the identifier to its canonical format.
|
|
26
|
+
#
|
|
27
|
+
# @param id [String, #to_s] the identifier to normalize
|
|
28
|
+
# @return [String] the normalized identifier
|
|
29
|
+
# @raise [InvalidFormatError] if the identifier format is invalid
|
|
30
|
+
def normalize!(id)
|
|
31
|
+
new(id).normalize!
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
data/lib/sec_id/occ.rb
CHANGED
|
@@ -3,9 +3,25 @@
|
|
|
3
3
|
require 'date'
|
|
4
4
|
|
|
5
5
|
module SecId
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
|
|
6
|
+
# OCC Option Symbol - standardized option symbol format used by Option Clearing Corporation.
|
|
7
|
+
# Format: 6-char underlying (padded) + 6-char date (YYMMDD) + type (C/P) + 8-digit strike (in mills).
|
|
8
|
+
#
|
|
9
|
+
# @note OCC identifiers have no check digit. The {#has_check_digit?} method returns false
|
|
10
|
+
# and validation includes both format and date parseability checks.
|
|
11
|
+
#
|
|
12
|
+
# @see https://en.wikipedia.org/wiki/Option_symbol#The_OCC_Option_Symbol
|
|
13
|
+
# @see https://web.archive.org/web/20120507220143/http://www.theocc.com/components/docs/initiatives/symbology/symbology_initiative_v1_8.pdf
|
|
14
|
+
#
|
|
15
|
+
# @example Validate an OCC symbol
|
|
16
|
+
# SecId::OCC.valid?('AAPL 210917C00150000') #=> true
|
|
17
|
+
#
|
|
18
|
+
# @example Build an OCC symbol from components
|
|
19
|
+
# occ = SecId::OCC.build(underlying: 'AAPL', date: '2021-09-17', type: 'C', strike: 150.0)
|
|
20
|
+
# occ.to_s #=> 'AAPL 210917C00150000'
|
|
21
|
+
class OCC < Base
|
|
22
|
+
include Normalizable
|
|
23
|
+
|
|
24
|
+
# Regular expression for parsing OCC symbol components.
|
|
9
25
|
ID_REGEX = /\A
|
|
10
26
|
(?<initial>
|
|
11
27
|
(?=.{1,6})(?<underlying>\d?[A-Z]{1,5}\d?)(?<padding>[ ]*))
|
|
@@ -14,90 +30,121 @@ module SecId
|
|
|
14
30
|
(?<strike_mills>\d{8})
|
|
15
31
|
\z/x
|
|
16
32
|
|
|
17
|
-
|
|
33
|
+
# @return [String, nil] the underlying security symbol (1-6 chars)
|
|
34
|
+
attr_reader :underlying
|
|
18
35
|
|
|
36
|
+
# @return [String, nil] the expiration date string in YYMMDD format
|
|
37
|
+
attr_reader :date_str
|
|
38
|
+
|
|
39
|
+
# @return [String, nil] the option type ('C' for call, 'P' for put)
|
|
40
|
+
attr_reader :type
|
|
41
|
+
|
|
42
|
+
# @return [String, nil] the strike price in mills (thousandths of a dollar, represented as an 8-digit string)
|
|
43
|
+
attr_reader :strike_mills
|
|
44
|
+
|
|
45
|
+
class << self
|
|
46
|
+
# Builds an OCC symbol from components.
|
|
47
|
+
#
|
|
48
|
+
# @param underlying [String] the underlying symbol (1-6 chars)
|
|
49
|
+
# @param date [String, Date] the expiration date
|
|
50
|
+
# @param type [String] 'C' for call or 'P' for put
|
|
51
|
+
# @param strike [Numeric, String] the strike price in dollars or 8-char mills string
|
|
52
|
+
# @return [OCC] a new OCC instance
|
|
53
|
+
# @raise [ArgumentError] if strike format is invalid
|
|
54
|
+
def build(underlying:, date:, type:, strike:)
|
|
55
|
+
date_obj = date.is_a?(Date) ? date : Date.parse(date)
|
|
56
|
+
strike_mills = normalize_strike_mills(strike)
|
|
57
|
+
|
|
58
|
+
new(compose_symbol(underlying, date_obj.strftime('%y%m%d'), type, strike_mills))
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Composes an OCC symbol string from its components.
|
|
62
|
+
#
|
|
63
|
+
# @param underlying [String] the underlying symbol
|
|
64
|
+
# @param date_str [String] the date in YYMMDD format
|
|
65
|
+
# @param type [String] 'C' or 'P'
|
|
66
|
+
# @param strike_mills [String, Integer] the strike in mills
|
|
67
|
+
# @return [String] the composed OCC symbol
|
|
68
|
+
def compose_symbol(underlying, date_str, type, strike_mills)
|
|
69
|
+
padded_underlying = underlying.to_s.ljust(6, "\s")
|
|
70
|
+
padded_strike = format('%08d', strike_mills.to_i)
|
|
71
|
+
|
|
72
|
+
"#{padded_underlying}#{date_str}#{type}#{padded_strike}"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
# @param strike [Numeric, String] strike price or 8-char mills string
|
|
78
|
+
# @return [String] 8-character strike mills string
|
|
79
|
+
# @raise [ArgumentError] if strike format is invalid
|
|
80
|
+
def normalize_strike_mills(strike)
|
|
81
|
+
case strike
|
|
82
|
+
when Numeric
|
|
83
|
+
format('%08d', (strike * 1000).to_i)
|
|
84
|
+
when String && /\A\d{8}\z/
|
|
85
|
+
strike
|
|
86
|
+
else
|
|
87
|
+
raise ArgumentError, 'Strike must be numeric or an 8-char string!'
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# @param symbol [String] the OCC symbol string to parse
|
|
19
93
|
def initialize(symbol)
|
|
20
|
-
symbol_parts = parse
|
|
21
|
-
@
|
|
94
|
+
symbol_parts = parse(symbol, upcase: false)
|
|
95
|
+
@identifier = symbol_parts[:initial]
|
|
22
96
|
@underlying = symbol_parts[:underlying]
|
|
23
97
|
@date_str = symbol_parts[:date]
|
|
24
98
|
@type = symbol_parts[:type]
|
|
25
99
|
@strike_mills = symbol_parts[:strike_mills]
|
|
100
|
+
@check_digit = nil
|
|
26
101
|
end
|
|
27
102
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
@date = Date.strptime(date_str, '%y%m%d') if date_str
|
|
32
|
-
rescue Date::Error
|
|
33
|
-
nil
|
|
103
|
+
# @return [Boolean] always false
|
|
104
|
+
def has_check_digit?
|
|
105
|
+
false
|
|
34
106
|
end
|
|
35
|
-
alias date_obj date
|
|
36
107
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
108
|
+
# Normalizes the OCC symbol to standard format with 6-char padded underlying and 8-digit strike.
|
|
109
|
+
#
|
|
110
|
+
# @return [String] the normalized OCC symbol
|
|
111
|
+
# @raise [InvalidFormatError] if the OCC symbol is invalid
|
|
112
|
+
def normalize!
|
|
113
|
+
raise InvalidFormatError, "OCC '#{full_number}' is invalid and cannot be normalized!" unless valid?
|
|
40
114
|
|
|
41
|
-
|
|
42
|
-
valid_format? && !date.nil?
|
|
115
|
+
@full_number = self.class.compose_symbol(underlying, date_str, type, strike_mills)
|
|
43
116
|
end
|
|
44
117
|
|
|
45
|
-
|
|
46
|
-
|
|
118
|
+
# @return [Boolean]
|
|
119
|
+
def valid?
|
|
120
|
+
valid_format? && !date.nil? # date must be parseable
|
|
47
121
|
end
|
|
48
122
|
|
|
49
|
-
|
|
50
|
-
|
|
123
|
+
# @return [Date, nil] the parsed date or nil if invalid
|
|
124
|
+
def date
|
|
125
|
+
return nil unless date_str
|
|
51
126
|
|
|
52
|
-
@
|
|
53
|
-
|
|
127
|
+
@date ||= Date.strptime(date_str, '%y%m%d')
|
|
128
|
+
rescue ArgumentError
|
|
129
|
+
nil
|
|
130
|
+
end
|
|
131
|
+
alias date_obj date
|
|
54
132
|
|
|
55
|
-
|
|
133
|
+
# @return [Float] strike price in dollars
|
|
134
|
+
def strike
|
|
135
|
+
@strike ||= strike_mills.to_i / 1000.0
|
|
56
136
|
end
|
|
57
137
|
|
|
138
|
+
# @return [String]
|
|
58
139
|
def to_s
|
|
59
|
-
|
|
140
|
+
full_number
|
|
60
141
|
end
|
|
61
142
|
alias to_str to_s
|
|
62
143
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def valid_format?(id)
|
|
69
|
-
new(id).valid_format?
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def normalize!(id)
|
|
73
|
-
new(id).normalize!
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
# rubocop:disable Metrics/MethodLength
|
|
77
|
-
def build(underlying:, date:, type:, strike:)
|
|
78
|
-
initial = underlying.to_s.ljust(6, "\s")
|
|
79
|
-
date = Date.parse(date.to_s) unless date.is_a?(Date)
|
|
80
|
-
|
|
81
|
-
case strike
|
|
82
|
-
when Numeric
|
|
83
|
-
strike_mills = format('%08d', (strike * 1000).to_i)
|
|
84
|
-
when /\A\d{8}\z/
|
|
85
|
-
strike_mills = strike
|
|
86
|
-
else
|
|
87
|
-
raise ArgumentError, 'Strike must be numeric or an 8-char string!'
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
symbol = "#{initial}#{date.strftime('%y%m%d')}#{type}#{strike_mills}"
|
|
91
|
-
new(symbol)
|
|
92
|
-
end
|
|
93
|
-
# rubocop:enable Metrics/MethodLength
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
private
|
|
97
|
-
|
|
98
|
-
def parse(symbol)
|
|
99
|
-
@full_symbol = symbol.to_s.strip
|
|
100
|
-
@full_symbol.match(ID_REGEX) || {}
|
|
144
|
+
# @deprecated Use {#full_number} instead
|
|
145
|
+
# @return [String]
|
|
146
|
+
def full_symbol
|
|
147
|
+
full_number
|
|
101
148
|
end
|
|
102
149
|
end
|
|
103
150
|
end
|
data/lib/sec_id/sedol.rb
CHANGED
|
@@ -1,32 +1,49 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module SecId
|
|
4
|
-
#
|
|
4
|
+
# Stock Exchange Daily Official List (SEDOL) - a 7-character alphanumeric code
|
|
5
|
+
# that identifies securities traded on the London Stock Exchange and other UK exchanges.
|
|
6
|
+
#
|
|
7
|
+
# Format: 6-character identifier + 1-digit check digit
|
|
8
|
+
# Note: SEDOL excludes vowels (A, E, I, O, U) to avoid forming words.
|
|
9
|
+
#
|
|
10
|
+
# @see https://en.wikipedia.org/wiki/SEDOL
|
|
11
|
+
#
|
|
12
|
+
# @example Validate a SEDOL
|
|
13
|
+
# SecId::SEDOL.valid?('B19GKT4') #=> true
|
|
14
|
+
#
|
|
15
|
+
# @example Calculate check digit
|
|
16
|
+
# SecId::SEDOL.check_digit('B19GKT') #=> 4
|
|
5
17
|
class SEDOL < Base
|
|
18
|
+
# Regular expression for parsing SEDOL components.
|
|
19
|
+
# Excludes vowels (A, E, I, O, U) from valid characters.
|
|
6
20
|
ID_REGEX = /\A
|
|
7
21
|
(?<identifier>[0-9BCDFGHJKLMNPQRSTVWXYZ]{6})
|
|
8
22
|
(?<check_digit>\d)?
|
|
9
23
|
\z/x
|
|
10
24
|
|
|
25
|
+
# Weights applied to each character position in the check digit calculation.
|
|
11
26
|
CHARACTER_WEIGHTS = [1, 3, 1, 7, 3, 9].freeze
|
|
12
27
|
|
|
28
|
+
# @param sedol [String] the SEDOL string to parse
|
|
13
29
|
def initialize(sedol)
|
|
14
30
|
sedol_parts = parse sedol
|
|
15
31
|
@identifier = sedol_parts[:identifier]
|
|
16
32
|
@check_digit = sedol_parts[:check_digit]&.to_i
|
|
17
33
|
end
|
|
18
34
|
|
|
35
|
+
# @return [Integer] the calculated check digit (0-9)
|
|
36
|
+
# @raise [InvalidFormatError] if the SEDOL format is invalid
|
|
19
37
|
def calculate_check_digit
|
|
20
|
-
|
|
21
|
-
raise InvalidFormatError, "SEDOL '#{full_number}' is invalid and check-digit cannot be calculated!"
|
|
22
|
-
end
|
|
23
|
-
|
|
38
|
+
validate_format_for_calculation!
|
|
24
39
|
mod10(weighted_sum)
|
|
25
40
|
end
|
|
26
41
|
|
|
27
42
|
private
|
|
28
43
|
|
|
29
|
-
# NOTE:
|
|
44
|
+
# NOTE: Not idiomatic Ruby, but optimized for performance.
|
|
45
|
+
#
|
|
46
|
+
# @return [Integer] the weighted sum
|
|
30
47
|
def weighted_sum
|
|
31
48
|
index = 0
|
|
32
49
|
sum = 0
|
|
@@ -39,6 +56,7 @@ module SecId
|
|
|
39
56
|
sum
|
|
40
57
|
end
|
|
41
58
|
|
|
59
|
+
# @return [Array<Integer>] array of digit values
|
|
42
60
|
def id_digits
|
|
43
61
|
@id_digits ||= identifier.each_char.map(&method(:char_to_digit))
|
|
44
62
|
end
|
data/lib/sec_id/version.rb
CHANGED
data/lib/sec_id.rb
CHANGED
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
require 'set'
|
|
4
4
|
require 'sec_id/version'
|
|
5
|
+
require 'sec_id/normalizable'
|
|
5
6
|
require 'sec_id/base'
|
|
6
7
|
require 'sec_id/isin'
|
|
7
8
|
require 'sec_id/cusip'
|
|
8
9
|
require 'sec_id/sedol'
|
|
9
10
|
require 'sec_id/figi'
|
|
11
|
+
require 'sec_id/lei'
|
|
12
|
+
require 'sec_id/iban'
|
|
10
13
|
require 'sec_id/cik'
|
|
11
14
|
require 'sec_id/occ'
|
|
12
15
|
|
data/sec_id.gemspec
CHANGED
|
@@ -11,7 +11,8 @@ Gem::Specification.new do |spec|
|
|
|
11
11
|
spec.email = ['leonid@svyatov.ru']
|
|
12
12
|
|
|
13
13
|
spec.summary = 'Validate securities identification numbers with ease!'
|
|
14
|
-
spec.description =
|
|
14
|
+
spec.description = 'Validate, calculate check digits, and parse components of securities identifiers. ' \
|
|
15
|
+
'Supports ISIN, CUSIP, SEDOL, FIGI, LEI, IBAN, CIK, and OCC standards.'
|
|
15
16
|
spec.homepage = 'https://github.com/svyatov/sec_id'
|
|
16
17
|
spec.license = 'MIT'
|
|
17
18
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sec_id
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 4.
|
|
4
|
+
version: 4.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Leonid Svyatov
|
|
@@ -9,8 +9,8 @@ bindir: bin
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies: []
|
|
12
|
-
description:
|
|
13
|
-
|
|
12
|
+
description: Validate, calculate check digits, and parse components of securities
|
|
13
|
+
identifiers. Supports ISIN, CUSIP, SEDOL, FIGI, LEI, IBAN, CIK, and OCC standards.
|
|
14
14
|
email:
|
|
15
15
|
- leonid@svyatov.ru
|
|
16
16
|
executables: []
|
|
@@ -25,7 +25,11 @@ files:
|
|
|
25
25
|
- lib/sec_id/cik.rb
|
|
26
26
|
- lib/sec_id/cusip.rb
|
|
27
27
|
- lib/sec_id/figi.rb
|
|
28
|
+
- lib/sec_id/iban.rb
|
|
29
|
+
- lib/sec_id/iban/country_rules.rb
|
|
28
30
|
- lib/sec_id/isin.rb
|
|
31
|
+
- lib/sec_id/lei.rb
|
|
32
|
+
- lib/sec_id/normalizable.rb
|
|
29
33
|
- lib/sec_id/occ.rb
|
|
30
34
|
- lib/sec_id/sedol.rb
|
|
31
35
|
- lib/sec_id/version.rb
|