Workdesk to stack research work on Security and/or Organization items
[smr.git] / gui / lib / smr / fetch.rb
blob3df8dff7f9252761544a6195adf5663d202a76ed
2 # This file is part of SMR.
4 # SMR is free software: you can redistribute it and/or modify it under the
5 # terms of the GNU General Public License as published by the Free Software
6 # Foundation, either version 3 of the License, or (at your option) any later
7 # version.
9 # SMR is distributed in the hope that it will be useful, but WITHOUT ANY
10 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
11 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License along with
14 # SMR.  If not, see <http://www.gnu.org/licenses/>.
18 # Namespace for reapers that do the actual fetching. Provides protected helpers
19 # for processing data.
20 module Smr::Reapers
22   ##
23   # List of symbols of reapers that can handle the given :security_type.
24   def get_suitable(security_type)
25      Smr::Reapers.constants.collect{ |reapername|
26          klass = ('Smr::Reapers::%s' % reapername).constantize
27          reapername if klass.type_supported? security_type
28      }.compact
29   end
30   module_function :get_suitable
32  protected
34    ##
35    # Translates special characters in :string.
36    def clean_special_chars(string)
37       replacement_rules = {
38           '\r' => '',
39           '\n' => '',
40           '\t' => '',
41       }
42       matcher = /#{replacement_rules.keys.join('|')}/
43       string.gsub(matcher, replacement_rules).strip
44    end
45 end
47 # load reapers, each will be part of Smr::Reapers namespace
48 # NOTE: loading with
50 #          Dir('reapers/*.rb').each{do |f| require f}
52 #       works in development but does nothing in production.
53 require 'ariva'
54 require 'finanzen_net'
55 require 'frankfurt'
56 require 'kitco'
57 require 'onvista'
58 require 'stuttgart'
59 require 'universal_investment'
62 # Fetch things from the internet. Things such as metadata of a Security or a
63 # Quote. Therefore this module is to be mixed into the Security model. Its
64 # methods operate on the attributes of a Security.
65 module Smr::Fetch
67     # Number of fails until :preferred_reaper is reset
68     PREFERRED_REAPER_FAILS = 5
70     ##
71     # tell whether given Smr::Reapers::* object is the preferred one.
72     def is_preferred_reaper? reaper
73        raise 'need Smr::Reapers::* object' unless reaper.is_a? Smr::Reapers
74        (reaper.class.to_s.split('::').last == self.preferred_reaper )
75     end
77     ##
78     # Uses Smr::Reapers to fetch metadata for :symbol and set object attributes
79     # appropriately.
80     #
81     # This will also create a new Security.type model and fills with the
82     # things discovered. In case the type model exists, its attributes will be
83     # updated.
84     #
85     # Returns true on success or false on failure.
86     #
87     # Reapers are called until one succeeds
88     def fetch_metadata
89         raise ':symbol must be set to fetch anything' if self.symbol.blank?
91         Smr::Reapers.constants.each do |reaperreapername|
92             reaperclass = ('Smr::Reapers::%s' % reaperreapername).constantize
93             next unless reaperclass.type_supported? self.type
94             reaper = reaperclass.new(self)
95             next unless reaper.respond_to? :metadata
97             logger.debug '== lookup metadata => %s with %s' % [ self.symbol, reaper.class ]
98             if reaper.metadata
99                 # metadata is found
100                 # - cut :brief string at first number because the numerical part
101                 #   is created form the metadata by the :typemodel
102                 self.brief = self.brief.split(/\d/).first
103                 return true
104             end
105         end
106         false
107     end
109     ##
110     # Users Smr::Reapers to obtain a recent quote for :symbol.
111     #
112     # Returns:
113     # - 0 success
114     # - 1 failure from the Reaper
115     # - 2 Quote was found but is older or of same time is the last known one
116     # - 3 no Reaper could handle that Security
117     #
118     # Reapers are called until one succeeds. The reaper succeeding will be
119     # remembered and is tried first at the next invokation of this method.
120     # This way each Security will adjust itself to a source thats most
121     # reliable, ie where there is active trading.
122     #
123     def update_quote
124         raise ':symbol must be set to update a quote' if self.symbol.blank?
125         retval = 3
126         reapers = Array.new
128         reapers << self.preferred_reaper.to_sym unless self.preferred_reaper.blank?
129         reapers << Smr::Reapers.get_suitable(self.type) unless self.preferred_reaper_locked
130         reapers = reapers.flatten.uniq
132         logger.debug '== lookup quote => %s using reapers %s' % [ self.symbol, reapers.inspect ]
133         reapers.each do |reapername|
134             logger.debug '  trying  %s' % reapername
135             begin reaper = ('Smr::Reapers::%s' % reapername).constantize.new(self)
136                 next unless reaper.respond_to?(:quote)
138                 lq = self.last_quote(Time.now, :false_if_not_found=>true, :no_caching=>true)
139                 if lq and lq.date + self.preferred_reaper_delay >= Time.now.to_i
140                     logger.debug '  ... delay not passed by, skipping.'
141                     next
142                 end
144                 q = reaper.quote
145             rescue
146                logger.fatal 'reaper crashed: %s on security.id=%i' % [ reaper.class, self.id ]
147                reset_reaper_config if is_preferred_reaper? reaper
148                next
149             end
151             if q.is_a?(Quote)
152                 if not lq or q.time > lq.time
153                     logger.debug'  ... quote found by %s' % reaper.class
154                     begin q.save!
155                     rescue
156                         logger.fatal 'Quote.safe! failed on security.id=%i reaper=%s msg=%s' % [self.id, reaper.class, $! ]
157                         reset_reaper_config
158                         retval = 1
159                     else
160                         reset_reaper_config reapername if self.preferred_reaper.blank?
161                         scale_delay :down
162                         retval = 0
163                     end
164                 else
165                     scale_delay
166                     retval = 2
167                 end
168                 break
169             else
170                 if is_preferred_reaper? reaper
171                     logger.debug'  ... failcount = %i' % read_attribute(:preferred_reaper_fail_count)
172                     write_attribute(:preferred_reaper_fail_count, self.preferred_reaper_fail_count + 1)
173                 end
174                 reset_reaper_config if read_attribute(:preferred_reaper_fail_count) >= PREFERRED_REAPER_FAILS
175                 retval = 1
176             end
177         end
179         logger.debug'  ... preferred is %s' % self.preferred_reaper
180         logger.debug'  retval = %s' % retval
182         save! if changed?
183         return retval
184     end
186   protected
188     ##
189     # scale the :preferred_reaper_delay :up or :down, starting with one hour
190     def scale_delay direction=:up
191         val = self.preferred_reaper_delay == 0 ? 1800 : self.preferred_reaper_delay
193         if direction == :down
194             val *= 1/3
195         else val *= 2 end
196         logger.debug '  ... delay scaled %s to %i seconds' % [direction, val]
197         write_attribute :preferred_reaper_delay, val
198     end
200 end # module