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 +4 -4
- data/lib/stocks.rb +29 -29
- data/lib/stocks/errors.rb +40 -3
- data/lib/stocks/historical.rb +45 -7
- data/lib/stocks/validators.rb +10 -0
- data/lib/stocks/validators/exists.rb +35 -5
- metadata +40 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e8a32a3160735bcc269c2da3567553382d56c810
|
4
|
+
data.tar.gz: b443130a3470403e57cd9bad82688c6b915c1f70
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
#
|
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
|
-
#
|
31
|
-
#
|
32
|
-
#
|
33
|
-
#
|
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
|
-
|
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
|
-
#
|
41
|
-
#
|
42
|
-
#
|
43
|
-
#
|
44
|
-
#
|
45
|
-
#
|
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(
|
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
|
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
|
-
|
6
|
-
|
7
|
-
|
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
|
|
data/lib/stocks/historical.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
34
|
-
raise
|
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
|
|
@@ -1,16 +1,46 @@
|
|
1
1
|
# Author:: Matt Fornaciari (mailto:mattforni@gmail.com)
|
2
2
|
# License:: MIT
|
3
3
|
|
4
|
-
|
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
|
-
|
11
|
-
|
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.
|
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-
|
11
|
+
date: 2015-05-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: activerecord
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
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: '
|
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
|
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
|
40
|
+
version: '4'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
42
|
+
name: yahoofinance
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: '
|
48
|
-
type: :
|
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: '
|
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:
|