stocks 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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: