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