stocks 0.0.1 → 0.0.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 14f408a3cbba0b2f34f833d80ffae64d800e96aa
4
- data.tar.gz: 29f789edf2db4baeb8d0b4b33ce49a4330150f92
3
+ metadata.gz: e8a32a3160735bcc269c2da3567553382d56c810
4
+ data.tar.gz: b443130a3470403e57cd9bad82688c6b915c1f70
5
5
  SHA512:
6
- metadata.gz: afef568d14ca8380dd14e52b2ce32dec19160ca4b72fd6d684c32c41764fe1f02164a5e5178d3f06a08b2afc45c0bccb444f1e3c33389e9f3a545929316881a4
7
- data.tar.gz: 87a7fbbf6601f962f2e07aabbb22ec251cd172a6a23ed5f39ab7bbe19b05a25816497765db1e62a1df526bf2c3632e03f40d49760dd3a414e385bdb8ea8b3be0
6
+ metadata.gz: 59f678bceefd7903c2444c1e452a67243668f7c0a8bbe3e2b6b2254b24b6e876a7c66fc2fc25ab5ba069d1806fd062e192d6ee841105a4db4b689126a2f8af37
7
+ data.tar.gz: 066a218fe46f8a47647945a37fa1f15ec33b577f90505558c63d4443eb1c6fb9e65c3c4cada46a0bd54fd6d2defa560cde2079464aff2a1d33682e574e7c5c3c
data/lib/stocks.rb CHANGED
@@ -2,6 +2,7 @@
2
2
  # License:: MIT
3
3
 
4
4
  # Require external libraries
5
+ require 'lrucache'
5
6
  require 'yahoofinance'
6
7
 
7
8
  # Require all Stocks related files
@@ -9,54 +10,53 @@ require 'stocks/errors'
9
10
  require 'stocks/historical'
10
11
  require 'stocks/validators/exists'
11
12
 
12
- # TODO test this module
13
- # TODO add a caching layer with 1-min TTL
13
+ # Provides an interface to do real-time analysis of stocks.
14
14
  module Stocks
15
- COMMISSION_RANGE = {greater_than_or_equal_to: 0}
16
- EPSILON = 0.00001
17
- NA = 'N/A'
18
- PRICE_RANGE = {greater_than: 0}
19
- PRICE_SCALE = 5
20
- PERCENTAGE_RANGE = {greater_than: 0, less_than: 100}
21
- QUANTITY_RANGE = {greater_than: 0}
22
-
23
- # TODO move to another lib module
24
- def self.equal?(value, other)
25
- (value-other).abs < EPSILON
26
- end
27
-
28
15
  # Determines whether or not the provided symbol exists.
29
16
  #
30
- # * *Args*:
31
- # - +symbol+ The symbol to evaluate
32
- # * *Returns*:
33
- # - Whether or not the provided symbol exists
17
+ # ===== *Args*
18
+ # - +symbol+ The symbol to evaluate for existence
19
+ # ===== *Returns*
20
+ # Whether or not the provided symbol exists
34
21
  def self.exists?(symbol)
35
- quote(symbol.upcase, [:date])[:date] != NA
22
+ symbol.upcase!
23
+ EXISTS_CACHE.fetch(symbol) { !quote(symbol, [:date])[:date].nil? }
24
+ end
25
+
26
+ # Raises an exception if the provided symbol does not exist.
27
+ #
28
+ # ===== *Args*
29
+ # - +symbol+ The symbol to evaluate for existence
30
+ # ===== *Raises*
31
+ # - +RetrievalError+ If the provided symbol does not exist
32
+ # Whether or not the provided symbol exists
33
+ def self.exists!(symbol)
34
+ raise RetrievalError.new(symbol) if !Stocks.exists?(symbol)
36
35
  end
37
36
 
38
37
  # Fetches the last trade for the provided symbol.
39
38
  #
40
- # * *Args*:
41
- # - +symbol+ The symbol to evaluate
42
- # * *Returns*:
43
- # - The last trade for the provided symbol
44
- # * *Raises*:
45
- # - +RetrievalError+ If the provided symbol does not exist
39
+ # ===== *Args*
40
+ # - +symbol+ The symbol to evaluate
41
+ # ===== *Returns*
42
+ # The last trade for the provided symbol
43
+ # ===== *Raises*
44
+ # - +RetrievalError+ If the provided symbol does not exist
46
45
  def self.last_trade(symbol)
47
46
  last_trade = quote(symbol)[:lastTrade]
48
- raise RetrievalError.new("Could not retrieve last trade for '#{symbol}'") if last_trade == 0 or last_trade == NA
47
+ raise RetrievalError.new(symbol) if last_trade.nil?
49
48
  last_trade
50
49
  end
51
50
 
52
51
  private
53
52
 
53
+ EXISTS_CACHE = LRUCache.new(max_size: 500, ttl: 1.month) # :nodoc:
54
+
54
55
  def self.quote(symbol, fields = [:lastTrade])
55
56
  data = {}
56
- # TODO use the fields map to decide which method to use
57
57
  standard_quote = YahooFinance::get_standard_quotes(symbol)[symbol]
58
58
  fields.each do |field|
59
- data[field] = standard_quote.send(field) rescue NA
59
+ data[field] = standard_quote.send(field) rescue nil
60
60
  end
61
61
  data
62
62
  end
data/lib/stocks/errors.rb CHANGED
@@ -2,10 +2,47 @@
2
2
  # License:: MIT
3
3
 
4
4
  module Stocks
5
- class RetrievalError < RuntimeError
6
- def new(message)
7
- super(message)
5
+ # An error raised when data retrieval fails
6
+ class RetrievalError < ArgumentError
7
+ def initialize(symbol) # :nodoc:
8
+ super(self.class.message(symbol))
8
9
  end
10
+
11
+ # Generates the error message that is to be displayed.
12
+ #
13
+ # ===== *Args*
14
+ # - +symbol+ The symbol to insert into the error message
15
+ # ===== *Returns*
16
+ # An error message representing the error
17
+ def self.message(symbol)
18
+ ERROR_MESSAGE % symbol
19
+ end
20
+
21
+ private
22
+
23
+ ERROR_MESSAGE = "Unable to retrieve quote for '%s'" # :nodoc:
24
+ end
25
+
26
+ # An error raised when an unsupported value is provided
27
+ class UnsupportedError < ArgumentError
28
+ def initialize(provided, supported) # :nodoc:
29
+ super(self.class.message(provided, supported))
30
+ end
31
+
32
+ # Generates the error message that is to be displayed.
33
+ #
34
+ # ===== *Args*
35
+ # - +provided+ The value that was provided
36
+ # - +provided+ A list of valid values
37
+ # ===== *Returns*
38
+ # An error message representing the error
39
+ def self.message(provided, supported)
40
+ ERROR_MESSAGE % [provided, supported.map { |s| "'#{s}'" }.join(', ')]
41
+ end
42
+
43
+ private
44
+
45
+ ERROR_MESSAGE = "'%s' is not supported. Try %s" # :nodoc:
9
46
  end
10
47
  end
11
48
 
@@ -4,6 +4,7 @@
4
4
  require 'active_support/core_ext/integer/time'
5
5
 
6
6
  module Stocks
7
+ # Provides an interface to do historical analysis of stocks.
7
8
  module Historical
8
9
  PERIODS = {
9
10
  one_month: {label: '1 Month', offset: 1.months},
@@ -11,29 +12,66 @@ module Stocks
11
12
  six_months: {label: '6 Months', offset: 6.months},
12
13
  one_year: {label: '1 Year', offset: 1.years},
13
14
  five_years: {label: '5 Years', offset: 5.years}
14
- }
15
+ } # :nodoc:
15
16
 
16
- def self.macd(symbol, days)
17
+ # Calculates the Moving Average Convergence Divergence.
18
+ #
19
+ # ===== *Args*
20
+ # +symbol+ The symbol for which to calculate the MACD
21
+ # +days+ The number of days for which to calculate the MACD
22
+ # ===== *Returns*
23
+ # An array of values for the number of days provided
24
+ # ===== *Raises*
25
+ # - +RetrievalError+ If the provided symbol does not exist
26
+ def self.macd(symbol, days = DEFAULT_DAYS)
17
27
  sma12 = self.sma(symbol, 12, days)
18
28
  sma26 = self.sma(symbol, 26, days)
19
29
  (0...days).collect { |day| sma12[day] - sma26[day] }
20
30
  end
21
31
 
22
- def self.sma(symbol, periods, days)
32
+ # Calculates the Simple Moving Average.
33
+ #
34
+ # ===== *Args*
35
+ # +symbol+ The symbol for which to calculate the SMA
36
+ # +periods+ The number of periods to include in the average
37
+ # +days+ The number of days for which to calculate the SMA
38
+ # ===== *Returns*
39
+ # An array of averages for the number of days provided
40
+ # ===== *Raises*
41
+ # - +RetrievalError+ If the provided symbol does not exist
42
+ def self.sma(symbol, days = DEFAULT_DAYS, periods = DEFAULT_PERIODS)
43
+ Stocks.exists!(symbol)
44
+
23
45
  sma = []
24
- days.downto(1).each do |day|
46
+ (days <= 0 ? DEFAULT_DAYS : days).downto(1).each do |day|
25
47
  date = Date.today - day
26
- quotes = YahooFinance::get_HistoricalQuotes(symbol, date - periods, date)
48
+ quotes = YahooFinance::get_HistoricalQuotes(symbol, date - (periods <= 0 ? DEFAULT_PERIODS : periods), date)
27
49
  sma << quotes.reduce(0) { |total, q| total += q.close } / quotes.size
28
50
  end
29
51
  sma
30
52
  end
31
53
 
54
+ # Determines whether or not the provided symbol exists.
55
+ #
56
+ # ===== *Args*
57
+ # +symbol+ The symbol for which to retrieve a quote
58
+ # +period+ The period the quote should span
59
+ # ===== *Returns*
60
+ # A quote for the provided symbol over the provided period
61
+ # ===== *Raises*
62
+ # - +RetrievalError+ If the provided symbol does not exist
63
+ # - +UnsupportedError+ If the provided period is not supported
32
64
  def self.quote(symbol, period)
33
- raise ArgumentError.new("Period must be provided for a historical quote") if period.nil?
34
- raise ArgumentError.new("'#{period}' is not a supported period") if !PERIODS.has_key?(period.to_sym)
65
+ Stocks.exists!(symbol)
66
+ raise UnsupportedError.new(period, PERIODS.keys) if !PERIODS.has_key?(period.try(:to_sym))
67
+
35
68
  YahooFinance::get_HistoricalQuotes(symbol, Date.today - PERIODS[period.to_sym][:offset], Date.today)
36
69
  end
70
+
71
+ private
72
+
73
+ DEFAULT_DAYS = 7 # :nodoc:
74
+ DEFAULT_PERIODS = 7 # :nodoc:
37
75
  end
38
76
  end
39
77
 
@@ -0,0 +1,10 @@
1
+ # Author:: Matt Fornaciari (mailto:mattforni@gmail.com)
2
+ # License:: MIT
3
+
4
+ module Stocks
5
+ # Provides an interface for validating ActiveRecord models.
6
+ module Validators
7
+ require 'active_model/validator'
8
+ end
9
+ end
10
+
@@ -1,16 +1,46 @@
1
1
  # Author:: Matt Fornaciari (mailto:mattforni@gmail.com)
2
2
  # License:: MIT
3
3
 
4
- require 'active_model/validator'
5
-
6
- module Stocks
4
+ module Stocks
7
5
  module Validators
6
+ # An ActiveRecord validator which validates that an attribute on a model is a
7
+ # valid ticker symbol. The default attribute is +:symbol+ but may be specified
8
+ # by defining +symbol_field+ when declaring the validator.
9
+ #
10
+ # Example Usage:
11
+ #
12
+ # require 'stocks/validators/exists'
13
+ #
14
+ # class Position < ActiveRecord::Base
15
+ # validates_with Stocks::Validators::Exists, symbol_field: :field_name
16
+ # end
8
17
  class Exists < ActiveModel::Validator
18
+
19
+ # Generates the error message that is inserted into +record.errors+.
20
+ #
21
+ # ===== *Args*
22
+ # - +symbol+ The symbol to insert into the error message
23
+ # ===== *Returns*
24
+ # An error message representing the validation failure
25
+ def self.message(symbol)
26
+ ERROR_MESSAGE % symbol
27
+ end
28
+
29
+ # Validates that a record have a valid symbol.
30
+ #
31
+ # ===== *Args*
32
+ # - +record+ The record to validate
9
33
  def validate(record)
10
- if (!Stocks.exists?(record.symbol))
11
- record.errors[:symbol] << "'#{record.symbol}' is not a valid symbol."
34
+ field = options[:symbol_field] || :symbol
35
+ symbol = record.send(field)
36
+ if (!Stocks.exists?(symbol))
37
+ record.errors[field] << self.class.message(symbol)
12
38
  end
13
39
  end
40
+
41
+ private
42
+
43
+ ERROR_MESSAGE = '%s is not a valid ticker symbol.' # :nodoc:
14
44
  end
15
45
  end
16
46
  end
metadata CHANGED
@@ -1,57 +1,71 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stocks
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthew Fornaciari
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-05-05 00:00:00.000000000 Z
11
+ date: 2015-05-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: yahoofinance
14
+ name: activerecord
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '1.2'
19
+ version: '4'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '1.2'
26
+ version: '4'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: activesupport
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '4.2'
33
+ version: '4'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '4.2'
40
+ version: '4'
41
41
  - !ruby/object:Gem::Dependency
42
- name: activerecord
42
+ name: yahoofinance
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '4.2'
48
- type: :development
47
+ version: '1.2'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.2'
55
+ - !ruby/object:Gem::Dependency
56
+ name: lrucache
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.1'
62
+ type: :runtime
49
63
  prerelease: false
50
64
  version_requirements: !ruby/object:Gem::Requirement
51
65
  requirements:
52
66
  - - "~>"
53
67
  - !ruby/object:Gem::Version
54
- version: '4.2'
68
+ version: '0.1'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: rspec
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -66,6 +80,20 @@ dependencies:
66
80
  - - "~>"
67
81
  - !ruby/object:Gem::Version
68
82
  version: '2'
83
+ - !ruby/object:Gem::Dependency
84
+ name: sqlite3
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.3'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.3'
69
97
  description: A programmatic approach to performing various financial analysis
70
98
  email: mattforni@gmail.com
71
99
  executables: []
@@ -75,6 +103,7 @@ files:
75
103
  - lib/stocks.rb
76
104
  - lib/stocks/errors.rb
77
105
  - lib/stocks/historical.rb
106
+ - lib/stocks/validators.rb
78
107
  - lib/stocks/validators/exists.rb
79
108
  homepage: http://rubygems.org/gems/stocks
80
109
  licenses: