finexclub 0.1.1
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.
- data/.document +5 -0
- data/.gitignore +21 -0
- data/LICENSE +20 -0
- data/README.rdoc +7 -0
- data/Rakefile +58 -0
- data/VERSION +1 -0
- data/bin/finexclub_updater +120 -0
- data/config.ru +4 -0
- data/finexclub.gemspec +100 -0
- data/lib/finexclub/app.rb +29 -0
- data/lib/finexclub/chart.rb +42 -0
- data/lib/finexclub/core.rb +36 -0
- data/lib/finexclub/document.rb +116 -0
- data/lib/finexclub/fabrication.rb +60 -0
- data/lib/finexclub/images.rb +38 -0
- data/lib/finexclub/manager.rb +103 -0
- data/lib/finexclub/signal.rb +27 -0
- data/lib/finexclub/signals/alpha.rb +28 -0
- data/lib/finexclub/signals/zeta.rb +26 -0
- data/lib/finexclub.rb +45 -0
- data/samples/egg.png +0 -0
- data/spec/finexclub/alpha_spec.rb +47 -0
- data/spec/finexclub/app_spec.rb +89 -0
- data/spec/finexclub/chart_spec.rb +51 -0
- data/spec/finexclub/core_spec.rb +91 -0
- data/spec/finexclub/document_spec.rb +203 -0
- data/spec/finexclub/images_spec.rb +49 -0
- data/spec/finexclub/manager_spec.rb +99 -0
- data/spec/finexclub/signal_spec.rb +38 -0
- data/spec/finexclub/zeta_spec.rb +52 -0
- data/spec/spec_helper.rb +34 -0
- metadata +185 -0
@@ -0,0 +1,103 @@
|
|
1
|
+
require "mongo"
|
2
|
+
require "ap"
|
3
|
+
|
4
|
+
module Finexclub
|
5
|
+
class Manager
|
6
|
+
|
7
|
+
attr_reader :core
|
8
|
+
attr_accessor :db
|
9
|
+
|
10
|
+
def initialize(core)
|
11
|
+
@core = core
|
12
|
+
end
|
13
|
+
|
14
|
+
def collection
|
15
|
+
raise("No db configured") if db.nil?
|
16
|
+
db.collection('charts')
|
17
|
+
end
|
18
|
+
|
19
|
+
def find(query, opts = {})
|
20
|
+
Cursor.new(core, Chart, collection.find(query, opts))
|
21
|
+
end
|
22
|
+
|
23
|
+
def find_one(query, opts = {})
|
24
|
+
return nil unless doc = collection.find_one(query, opts)
|
25
|
+
chart = Chart.new(core)
|
26
|
+
chart.build(doc)
|
27
|
+
chart
|
28
|
+
end
|
29
|
+
|
30
|
+
def last_updated
|
31
|
+
c = collection.find({}, :fields => {:updated => 1}, :sort => [['updated', :desc]], :limit => 1).first
|
32
|
+
c.nil?? nil : Time.at(c["updated"])
|
33
|
+
end
|
34
|
+
|
35
|
+
def add_signal(signal_type, params)
|
36
|
+
signal = Signal.build(core, signal_type, params.merge(:updated => Time.now))
|
37
|
+
c = {
|
38
|
+
:symbol => signal.symbol,
|
39
|
+
:date => signal.updated_at.strftime("%Y-%m-%d")
|
40
|
+
}
|
41
|
+
collection.update(c, {"$push" => {signal_type => signal.to_doc}, "$set" => {:updated => signal.updated}}, :upsert => true)
|
42
|
+
end
|
43
|
+
|
44
|
+
class Cursor
|
45
|
+
include Enumerable
|
46
|
+
attr_accessor :mongo_cursor
|
47
|
+
attr_reader :core
|
48
|
+
|
49
|
+
def initialize(core, obj_class, mongo_cursor)
|
50
|
+
@core = core
|
51
|
+
@obj_class = obj_class
|
52
|
+
@mongo_cursor = mongo_cursor
|
53
|
+
end
|
54
|
+
|
55
|
+
#def method_missing(name, *args)
|
56
|
+
#@mongo_cursor.send name, *args
|
57
|
+
#end
|
58
|
+
|
59
|
+
# Is the cursor empty? This method is much more efficient than doing cursor.count == 0
|
60
|
+
def empty?
|
61
|
+
@mongo_cursor.has_next? == false
|
62
|
+
end
|
63
|
+
|
64
|
+
def next_document
|
65
|
+
if doc = @mongo_cursor.next_document
|
66
|
+
obj = @obj_class.new(core)
|
67
|
+
obj.build(doc)
|
68
|
+
obj
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
alias :next :next_document
|
73
|
+
|
74
|
+
def each
|
75
|
+
@mongo_cursor.each do |doc|
|
76
|
+
obj = @obj_class.new(core)
|
77
|
+
obj.build(doc)
|
78
|
+
yield(obj)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def current_limit
|
83
|
+
@mongo_cursor.limit
|
84
|
+
end
|
85
|
+
|
86
|
+
def limit(number_to_return)
|
87
|
+
@mongo_cursor.limit(number_to_return); self
|
88
|
+
end
|
89
|
+
|
90
|
+
def current_skip
|
91
|
+
@mongo_cursor.skip
|
92
|
+
end
|
93
|
+
|
94
|
+
def skip(number_to_skip)
|
95
|
+
@mongo_cursor.skip(number_to_skip); self
|
96
|
+
end
|
97
|
+
|
98
|
+
def sort(key_or_list, direction = nil)
|
99
|
+
@mongo_cursor.sort(key_or_list, direction); self
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Finexclub
|
2
|
+
class Signal
|
3
|
+
include Finexclub::Document
|
4
|
+
attr_reader :core
|
5
|
+
|
6
|
+
def initialize(core, meta = {})
|
7
|
+
@core = core
|
8
|
+
apply_meta(meta)
|
9
|
+
end
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def handler_for(signal_type)
|
13
|
+
#class_name = signal_type.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
|
14
|
+
class_name = signal_type.to_s.gsub(/^(.)/) { $1.upcase }
|
15
|
+
Finexclub::Signals.const_get(class_name)
|
16
|
+
end
|
17
|
+
|
18
|
+
def build(core, signal_type, params)
|
19
|
+
signal = handler_for(signal_type).new(core)
|
20
|
+
signal.build(params)
|
21
|
+
signal
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Finexclub
|
2
|
+
module Signals
|
3
|
+
|
4
|
+
class Alpha < Signal
|
5
|
+
field :symbol, :symbol
|
6
|
+
field :updated, :timestamp
|
7
|
+
field :index, :integer
|
8
|
+
field :direction, :integer
|
9
|
+
|
10
|
+
doc_fields :updated, :index, :direction
|
11
|
+
|
12
|
+
def trend
|
13
|
+
direction == 1 ? :bullish : :bearish
|
14
|
+
end
|
15
|
+
|
16
|
+
def stability
|
17
|
+
case index
|
18
|
+
when 100 then :confirmed
|
19
|
+
when 90..99 then :high
|
20
|
+
when 80..89 then :medium
|
21
|
+
when 70..79 then :low
|
22
|
+
else :unstable
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Finexclub
|
2
|
+
module Signals
|
3
|
+
|
4
|
+
class Zeta < Signal
|
5
|
+
field :symbol, :symbol
|
6
|
+
field :updated, :timestamp
|
7
|
+
|
8
|
+
field :up_support, :float
|
9
|
+
field :up_resist, :float
|
10
|
+
field :down_support, :float
|
11
|
+
field :down_resist, :float
|
12
|
+
field :screenshot, :image
|
13
|
+
|
14
|
+
doc_fields :up_support, :up_resist, :down_support, :down_resist, :screenshot, :updated
|
15
|
+
|
16
|
+
def up_channel?
|
17
|
+
up_support != 0 && up_resist != 0
|
18
|
+
end
|
19
|
+
|
20
|
+
def down_channel?
|
21
|
+
down_support != 0 && down_resist != 0
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
data/lib/finexclub.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
# AUTOLOAD EVERYTHING IN THE FINEXCLUB DIRECTORY TREE
|
2
|
+
|
3
|
+
# The convention is that dirs are modules
|
4
|
+
# so declare them here and autoload any modules/classes inside them
|
5
|
+
# All paths here are absolute
|
6
|
+
def camelize(path)
|
7
|
+
# e.g. 'test/this_one' => Test::ThisOne
|
8
|
+
"#{path}".
|
9
|
+
chomp('/').
|
10
|
+
gsub('/','::').
|
11
|
+
gsub(/([^a-z])(\w)/){ "#{$1}#{$2.upcase}" }.
|
12
|
+
gsub('_','').
|
13
|
+
sub(/^(\w)/){ $1.upcase }
|
14
|
+
end
|
15
|
+
def autoload_files_in_dir(path, namespace)
|
16
|
+
# Define the module
|
17
|
+
eval("module #{namespace}; end")
|
18
|
+
# Autoload modules/classes in that module
|
19
|
+
Dir.glob("#{path}/*.rb").each do |file|
|
20
|
+
file = File.expand_path(file)
|
21
|
+
sub_const_name = camelize( File.basename(file, '.rb') )
|
22
|
+
eval("#{namespace}.autoload('#{sub_const_name}', '#{file}')")
|
23
|
+
end
|
24
|
+
# Recurse on subdirectories
|
25
|
+
Dir.glob("#{path}/*/").each do |dir|
|
26
|
+
sub_namespace = camelize( File.basename(dir) )
|
27
|
+
autoload_files_in_dir(dir, "#{namespace}::#{sub_namespace}")
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
autoload_files_in_dir("#{File.dirname(__FILE__)}/finexclub", 'Finexclub')
|
32
|
+
|
33
|
+
require 'forwardable'
|
34
|
+
|
35
|
+
module Finexclub
|
36
|
+
class << self
|
37
|
+
extend Forwardable
|
38
|
+
def_delegators :core, :store, :find, :configure
|
39
|
+
def_delegators :core, :images, :signals
|
40
|
+
|
41
|
+
def core
|
42
|
+
Core.instance
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/samples/egg.png
ADDED
Binary file
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
2
|
+
|
3
|
+
describe 'Finexclub::Signals::Alpha' do
|
4
|
+
before do
|
5
|
+
@core = mock_core
|
6
|
+
@alpha = Finexclub::Signals::Alpha.new(@core)
|
7
|
+
end
|
8
|
+
|
9
|
+
it 'should provide attribute accessors' do
|
10
|
+
@alpha.build("direction" => 1, "index" => 100)
|
11
|
+
@alpha.direction.should == 1
|
12
|
+
@alpha.index.should == 100
|
13
|
+
end
|
14
|
+
|
15
|
+
{
|
16
|
+
1 => :bullish,
|
17
|
+
-1 => :bearish,
|
18
|
+
0 => :bearish
|
19
|
+
}.each do |direction, trend|
|
20
|
+
it "should return #{trend} trend when direction is #{direction}" do
|
21
|
+
@alpha.direction = direction
|
22
|
+
@alpha.trend.should == trend
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
{
|
27
|
+
:confirmed => 100..100,
|
28
|
+
:high => 90..99,
|
29
|
+
:medium => 80..89,
|
30
|
+
:low => 70..79,
|
31
|
+
:unstable => 0..60
|
32
|
+
}.each do |stability, range|
|
33
|
+
it "should return #{stability} stability when index within #{range}" do
|
34
|
+
@alpha.index = range.first
|
35
|
+
@alpha.stability.should == stability
|
36
|
+
|
37
|
+
@alpha.index = range.last
|
38
|
+
@alpha.stability.should == stability
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'should export valid doc' do
|
43
|
+
@alpha.build("direction" => 1, "index" => 100, "updated" => 123)
|
44
|
+
@alpha.to_doc.should == {:updated => 123, :index => 100, :direction => 1}
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
@@ -0,0 +1,89 @@
|
|
1
|
+
ENV['RACK_ENV'] = 'test'
|
2
|
+
|
3
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
4
|
+
require 'rack/test'
|
5
|
+
|
6
|
+
include Rack::Test::Methods
|
7
|
+
include Finexclub::Fabrication
|
8
|
+
|
9
|
+
Finexclub.configure do |f|
|
10
|
+
f.images.screenshot_path = TEST_IMAGES_PATH
|
11
|
+
f.signals.db = Mongo::Connection.new.db("finexclub_test")
|
12
|
+
end
|
13
|
+
|
14
|
+
def app
|
15
|
+
Finexclub::App
|
16
|
+
end
|
17
|
+
|
18
|
+
def charts
|
19
|
+
Finexclub.signals.collection
|
20
|
+
end
|
21
|
+
|
22
|
+
def make_request(url, opts = {})
|
23
|
+
post url, opts
|
24
|
+
last_response
|
25
|
+
end
|
26
|
+
|
27
|
+
def date(ts = nil)
|
28
|
+
ts ||= Time.now.utc
|
29
|
+
ts.strftime("%Y-%m-%d")
|
30
|
+
end
|
31
|
+
|
32
|
+
def alpha_params()
|
33
|
+
params = {}
|
34
|
+
params[:alpha] = Finexclub::Chart::SYMBOLS.inject([]) {|alpha, symbol| alpha << raw_alpha_hash("symbol" => symbol.upcase)}
|
35
|
+
params
|
36
|
+
end
|
37
|
+
|
38
|
+
def zeta_params(options = {})
|
39
|
+
#options[:symbol] ||= "EURUSD"
|
40
|
+
params = {}
|
41
|
+
params[:zeta] = [raw_zeta_hash(options)]
|
42
|
+
params
|
43
|
+
end
|
44
|
+
|
45
|
+
describe 'Finexclub::App' do
|
46
|
+
describe 'saving Alpha' do
|
47
|
+
before do
|
48
|
+
charts.remove
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'should create first signal' do
|
52
|
+
response = make_request('/alpha', alpha_params)
|
53
|
+
response.status.should == 200
|
54
|
+
|
55
|
+
charts.find().count.should == 28
|
56
|
+
charts.find_one(:date => date, :symbol => 'eurusd')["alpha"].size.should == 1
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'should add more alpha signals' do
|
60
|
+
Timecop.travel
|
61
|
+
response = make_request('/alpha', alpha_params)
|
62
|
+
response.status.should == 200
|
63
|
+
|
64
|
+
Timecop.travel(1*60*60) #travel 1 hour later
|
65
|
+
response = make_request('/alpha', alpha_params)
|
66
|
+
response.status.should == 200
|
67
|
+
charts.find().count.should == 28
|
68
|
+
charts.find_one(:date => date, :symbol => 'eurusd')["alpha"].size.should == 2
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe 'saving Zeta' do
|
73
|
+
before do
|
74
|
+
charts.remove
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'should create first zeta signals' do
|
78
|
+
response = make_request('/zeta', zeta_params)
|
79
|
+
response.status.should == 200
|
80
|
+
|
81
|
+
charts.find().count.should == 1
|
82
|
+
charts.find_one(:date => date, :symbol => 'eurusd')["zeta"].size.should == 1
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
describe 'keep alive ping' do
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
2
|
+
|
3
|
+
describe 'Finexclub::Chart' do
|
4
|
+
before do
|
5
|
+
@core = mock_core
|
6
|
+
@chart = Finexclub::Chart.new(@core)
|
7
|
+
end
|
8
|
+
|
9
|
+
describe 'Instance' do
|
10
|
+
before do
|
11
|
+
@a1, @a2 = mock('a1'), mock('a2')
|
12
|
+
@z1, @z2 = mock('z1'), mock('z2')
|
13
|
+
@ts = Time.now.to_i
|
14
|
+
|
15
|
+
chart_doc = {
|
16
|
+
:symbol => 'eurusd',
|
17
|
+
:date => '2010-09-08',
|
18
|
+
:updated => @ts,
|
19
|
+
:alpha => [ @a1, @a2 ],
|
20
|
+
:zeta => [ @z1, @z2 ]
|
21
|
+
}
|
22
|
+
@chart.build(chart_doc)
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'should have attribute accessors' do
|
26
|
+
@chart.symbol.should == 'eurusd'
|
27
|
+
@chart.date.should == '2010-09-08'
|
28
|
+
@chart.updated.should == @ts
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'should return all alpha signals' do
|
32
|
+
@chart.signals(:alpha).should.equal [@a1, @a2]
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'should return all zeta signals' do
|
36
|
+
@chart.signals(:zeta).should.equal [@z1, @z2]
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'should return latest alpha signal as Alpha' do
|
40
|
+
Finexclub::Signal.should.receive(:build).with(@core, :alpha, @a2).and_return(a = mock('alpha'))
|
41
|
+
@chart.alpha.should == a
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'should return latest zeta signals as Zeta' do
|
45
|
+
Finexclub::Signal.should.receive(:build).with(@core, :zeta, @z2).and_return(z = mock('zeta'))
|
46
|
+
@chart.zeta.should == z
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
2
|
+
|
3
|
+
describe 'Finexclub::Core' do
|
4
|
+
|
5
|
+
describe '.instance' do
|
6
|
+
it 'should create instance if it does not already exists' do
|
7
|
+
app = Finexclub::Core.instance
|
8
|
+
app.should.be.instance_of Finexclub::Core
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'should return an existing instance' do
|
12
|
+
app = Finexclub::Core.instance
|
13
|
+
Finexclub::Core.instance.should == app
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'should work with Finexclub.core' do
|
17
|
+
Finexclub.core.should == Finexclub::Core.instance
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
describe '.new' do
|
22
|
+
it 'should not be callable' do
|
23
|
+
lambda {
|
24
|
+
Finexclub::Core.new
|
25
|
+
}.should.raise(NoMethodError)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe '.configure' do
|
30
|
+
before do
|
31
|
+
@core = test_core
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'should allow to configure' do
|
35
|
+
@core.configure do |f|
|
36
|
+
f.should == @core
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe '#images' do
|
42
|
+
before do
|
43
|
+
@core = test_core
|
44
|
+
end
|
45
|
+
it 'should return Images' do
|
46
|
+
@core.images.class.should == Finexclub::Images
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe '#signals' do
|
51
|
+
before do
|
52
|
+
@core = test_core
|
53
|
+
end
|
54
|
+
it 'should return Manager' do
|
55
|
+
@core.signals.class.should == Finexclub::Manager
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe '#find' do
|
60
|
+
before do
|
61
|
+
@core = test_core
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'should fetch all charts for given date' do
|
65
|
+
@core.signals.should.receive(:find).with({:date => "2010-10-01"}).and_return(c = mock('cursor'))
|
66
|
+
@core.find(:all, "2010-10-01").should == c
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'should allow fetching signals by date and symbol' do
|
70
|
+
@core.signals.should.receive(:find_one).with({:date => "2010-10-01", :symbol => "eurusd"}).and_return(chart = mock('chart'))
|
71
|
+
@core.find("eurusd", "2010-10-01").should == chart
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
describe '#store' do
|
76
|
+
before do
|
77
|
+
@core = test_core
|
78
|
+
end
|
79
|
+
it 'should store each signal given an array of params' do
|
80
|
+
@core.signals.should.receive(:add_signal).with(:alpha,p1=mock('p1'))
|
81
|
+
@core.signals.should.receive(:add_signal).with(:alpha,p2=mock('p2'))
|
82
|
+
@core.store(:alpha, [p1, p2])
|
83
|
+
end
|
84
|
+
it 'should store signal given signle hash of params' do
|
85
|
+
@core.signals.should.receive(:add_signal).with(:alpha,p1=mock('p1'))
|
86
|
+
@core.store(:alpha, p1)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
|