lunaris 0.0.0

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.
Files changed (32) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +1 -0
  3. data/lib/lunaris/autoloader.rb +42 -0
  4. data/lib/lunaris/base.rb +11 -0
  5. data/lib/lunaris/config/application.rb +21 -0
  6. data/lib/lunaris/config/logger.rb +39 -0
  7. data/lib/lunaris/config/sentry_client.rb +20 -0
  8. data/lib/lunaris/helpers/file_helper.rb +21 -0
  9. data/lib/lunaris/helpers/loggable.rb +15 -0
  10. data/lib/lunaris/resources/builders/crypto_market_data_builder.rb +98 -0
  11. data/lib/lunaris/resources/clients/aws.rb +34 -0
  12. data/lib/lunaris/resources/clients/binance.rb +22 -0
  13. data/lib/lunaris/resources/clients/fgi.rb +35 -0
  14. data/lib/lunaris/resources/controllers/market_automation_controller.rb +17 -0
  15. data/lib/lunaris/resources/endpoint.rb +39 -0
  16. data/lib/lunaris/resources/jobs/market_automation_job.rb +39 -0
  17. data/lib/lunaris/resources/models/crypto.rb +41 -0
  18. data/lib/lunaris/resources/models/crypto_market_data.rb +19 -0
  19. data/lib/lunaris/resources/models/indicator.rb +68 -0
  20. data/lib/lunaris/resources/models/ticker.rb +31 -0
  21. data/lib/lunaris/resources/repositories/file_storage_repository.rb +35 -0
  22. data/lib/lunaris/resources/services/crypto_market_data_pipeline.rb +47 -0
  23. data/lib/lunaris/resources/services/indicator_service.rb +59 -0
  24. data/lib/lunaris/resources/services/indicators/base_indicator_service.rb +43 -0
  25. data/lib/lunaris/resources/services/indicators/momentum.rb +15 -0
  26. data/lib/lunaris/resources/services/indicators/sentiment.rb +58 -0
  27. data/lib/lunaris/resources/services/indicators/trend_following.rb +15 -0
  28. data/lib/lunaris/resources/services/indicators/volatility.rb +15 -0
  29. data/lib/lunaris/resources/services/indicators/volume_based.rb +15 -0
  30. data/lib/lunaris/version.rb +5 -0
  31. data/lib/lunaris.rb +25 -0
  32. metadata +242 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 18415f6f131d128759635c746a46116dd5fae820875baecd27211cdfcf2a6e63
4
+ data.tar.gz: 228861eb236f3d8eae1e8e78352a28389eff617068eff54f9935004ffe481052
5
+ SHA512:
6
+ metadata.gz: 0fb5fc4c6a1c547aae1dd0560d99634055ca35c4559e31c4f8a9d296871b963d0c3aaba8ff0d3ecbe692f698e37f23b33acd485345d2b1e170012b73f9b02191
7
+ data.tar.gz: 3197810c146528e3094dd693876eecd2cf4f065ae9d369515a94e073438ae0e824dfe7294eb6306f7637b106efef55dd55d0df23165d79faa8ab580bb2db1045
data/README.md ADDED
@@ -0,0 +1 @@
1
+ # next-gen-ruby
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zeitwerk'
4
+
5
+ module Lunaris
6
+ class Autoloader
7
+ class Inflector < ::Zeitwerk::Inflector
8
+ INFLECTIONS_MAP = {
9
+ version: 'VERSION'
10
+ }.freeze
11
+
12
+ def camelize(basename, _abspath)
13
+ INFLECTIONS_MAP[basename.to_sym] || super
14
+ end
15
+ end
16
+
17
+ class << self
18
+ LIB_PATH = ::File.dirname(__dir__).freeze
19
+
20
+ def setup!
21
+ loader = ::Zeitwerk::Loader.new
22
+ loader.tag = 'lunaris'
23
+ loader.inflector = Inflector.new
24
+ loader.push_dir(LIB_PATH)
25
+ loader.collapse(::File.join(LIB_PATH, 'lunaris', 'resources'))
26
+
27
+ ignored_paths.each { |path| loader.ignore(path) }
28
+
29
+ loader.setup
30
+ end
31
+
32
+ private
33
+
34
+ def ignored_paths
35
+ [
36
+ ::File.join(LIB_PATH, 'lunaris', 'patches'),
37
+ ::File.join(LIB_PATH, 'lunaris', 'config', 'schedule.rb')
38
+ ].freeze
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lunaris
4
+ class Base
5
+ class << self
6
+ attr_accessor :app_info
7
+ end
8
+
9
+ self.app_info = ::ENV['APP_INFO'] || "#{::Lunaris::NAME} V #{::Lunaris.version}"
10
+ end
11
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+ require 'tzinfo'
5
+
6
+ module Lunaris
7
+ module Config
8
+ module Application
9
+ def self.set_timezone(timezone) # rubocop: disable Naming/AccessorMethodName
10
+ ENV['TZ'] = timezone
11
+ end
12
+
13
+ def self.timestamp_to_date(timestamp)
14
+ utc_time = Time.at(timestamp).utc
15
+ tz = TZInfo::Timezone.get('GMT')
16
+
17
+ tz.utc_to_local(utc_time)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+ require 'singleton'
5
+ require 'date'
6
+
7
+ module Lunaris
8
+ module Config
9
+ class Logger
10
+ include Singleton
11
+ include Lunaris::Helpers::FileHelper
12
+
13
+ def initialize
14
+ log_directory = defined?(RSpec) ? 'spec' : File.expand_path('', Dir.pwd)
15
+ log_file = "#{log_directory}/#{base_path_hour}/#{file_name}"
16
+ FileUtils.mkdir_p(File.dirname(log_file))
17
+
18
+ @logger = ::Logger.new(log_file)
19
+ @logger.level = ::Logger::DEBUG
20
+ end
21
+
22
+ def info(message)
23
+ @logger.info(message)
24
+ end
25
+
26
+ def error(message)
27
+ @logger.error(message)
28
+ end
29
+
30
+ private
31
+
32
+ def file_name
33
+ env = ENV['APP_ENV'] || 'development'
34
+
35
+ "#{env}.log"
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sentry-ruby'
4
+
5
+ module Lunaris
6
+ module Config
7
+ class SentryClient
8
+ def self.setup
9
+ return unless defined?(Sentry)
10
+
11
+ Sentry.init do |config|
12
+ config.dsn = ENV.fetch('SENTRY_DSN', nil)
13
+ config.traces_sample_rate = 0.2
14
+ config.environment = ENV.fetch('APP_ENV', 'development')
15
+ config.send_default_pii = true
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lunaris
4
+ module Helpers
5
+ module FileHelper
6
+ def initialize_time
7
+ @today ||= DateTime.parse(ENV.fetch('DATETIME', DateTime.now))
8
+ end
9
+
10
+ def base_path_day
11
+ initialize_time
12
+ "data/#{@today.strftime('%Y-%m-%d')}"
13
+ end
14
+
15
+ def base_path_hour
16
+ initialize_time
17
+ "#{base_path_day}/#{@today.strftime('%H')}"
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lunaris
4
+ module Helpers
5
+ module Loggable
6
+ def logger
7
+ @logger ||= Config::Logger.instance
8
+ end
9
+
10
+ def log_info(message)
11
+ logger.info(message)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lunaris
4
+ module Builders
5
+ class CryptoMarketDataBuilder
6
+ attr_reader :crypto, :tickers, :indicator_obj
7
+
8
+ INDICATOR_TYPES = %i[sma_values ema_values macd_values rsi_values sr_values tsi_values bb_values atr_values
9
+ kc_values obv_values fgi_values].freeze
10
+
11
+ def initialize(crypto, tickers, indicators)
12
+ @crypto = crypto
13
+ @tickers = tickers
14
+ @indicator_obj = Models::Indicator.new(indicators: indicators)
15
+ end
16
+
17
+ def build
18
+ crypto.to_h.merge(data: build_tickers_data)
19
+ end
20
+
21
+ private
22
+
23
+ def build_tickers_data
24
+ Models::Ticker.sorted_by_date(tickers).first(6).map do |ticker|
25
+ ticker_data(ticker)
26
+ end
27
+ end
28
+
29
+ def build_values(type, filtered_indicators)
30
+ case type
31
+ when :fgi_values
32
+ day_builder(filtered_indicators)
33
+ else
34
+ datetime_builder(filtered_indicators)
35
+ end
36
+ end
37
+
38
+ def datetime_builder(values)
39
+ values.each_slice(2).with_object({}) do |(indicator, index), data|
40
+ process_indicator(indicator, index, data)
41
+ end
42
+ end
43
+
44
+ def filter_indicators(type, ticker_datetime)
45
+ method = type == :fgi_values ? :filter_by_day : :filter_by_datetime
46
+ indicator_obj.send(method, indicator_obj.indicators.send(type), ticker_datetime)
47
+ end
48
+
49
+ def format_attribute(variable, index)
50
+ attribute = variable.to_s.delete('@').to_sym
51
+ attribute = format_sma_ema(attribute, index) if %i[sma ema].include?(attribute)
52
+ attribute
53
+ end
54
+
55
+ def format_indicators(ticker_datetime)
56
+ INDICATOR_TYPES.each_with_object({}) do |type, hash|
57
+ filtered_indicators = filter_indicators(type, ticker_datetime)
58
+ hash[type] = format_to_hash(build_values(type, filtered_indicators))
59
+ end
60
+ end
61
+
62
+ def format_sma_ema(attribute, index)
63
+ suffix = (index.even? ? '10' : '20')
64
+ :"#{attribute}#{suffix}"
65
+ end
66
+
67
+ def format_to_hash(values)
68
+ return values if values.is_a?(Hash)
69
+
70
+ values.reduce({}, :merge)
71
+ end
72
+
73
+ def process_indicator(indicator, index, data)
74
+ indicator.instance_variables.each do |variable|
75
+ attribute = format_attribute(variable, index)
76
+ next if attribute == :date_time
77
+
78
+ data[attribute] = indicator.instance_variable_get(variable)
79
+ end
80
+ end
81
+
82
+ def ticker_data(ticker)
83
+ ticker.to_h.merge(format_indicators(ticker.date))
84
+ end
85
+
86
+ def day_builder(values)
87
+ values.map do |d|
88
+ {
89
+ value: d.value,
90
+ value_classification: d.value_classification,
91
+ timestamp: Config::Application.timestamp_to_date(d.timestamp.to_i),
92
+ time_until_update: d.time_until_update
93
+ }
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aws-sdk-lambda'
4
+
5
+ module Lunaris
6
+ module Clients
7
+ class Aws
8
+ include Lunaris::Helpers::Loggable
9
+
10
+ attr_reader :client
11
+
12
+ def initialize
13
+ @client = ::Aws::Lambda::Client.new(
14
+ access_key_id: ENV.fetch('AWS_ACCESS_KEY_ID'),
15
+ secret_access_key: ENV.fetch('AWS_SECRET_ACCESS_KEY'),
16
+ region: ENV.fetch('AWS_REGION', 'eu-west-2')
17
+ )
18
+ end
19
+
20
+ def invoke(params)
21
+ response = client.invoke({
22
+ function_name: 'Lunaris',
23
+ invocation_type: 'RequestResponse',
24
+ log_type: 'Tail',
25
+ payload: JSON.generate(params)
26
+ })
27
+
28
+ payload = JSON.parse(response.payload.string)
29
+ log_info("Lambda call with params: #{params} - status: #{payload['statusCode']}")
30
+ payload['body']
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rest-client'
4
+ require 'json'
5
+
6
+ module Lunaris
7
+ module Clients
8
+ class Binance
9
+ attr_reader :client, :params
10
+
11
+ def initialize(params)
12
+ @params = params
13
+ @client = Clients::Aws.new
14
+ end
15
+
16
+ def candlestick
17
+ lambda_params = { lambda: { class: 'Clients::Binance', method: 'candlestick' } }
18
+ client.invoke(lambda_params.merge(params))
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rest-client'
4
+ require 'json'
5
+ require 'fileutils'
6
+
7
+ module Lunaris
8
+ module Clients
9
+ class Fgi
10
+ attr_accessor :params
11
+ attr_reader :client, :logger, :today
12
+
13
+ def initialize(params)
14
+ @params = params
15
+ @client = Clients::Aws.new
16
+ @today = Date.today
17
+ end
18
+
19
+ def values
20
+ datetime = DateTime.parse(ENV.fetch('DATETIME', nil))
21
+ params[:limit] = 10_000 if datetime.to_date < today
22
+ base_path = "data/#{datetime.strftime('%Y-%m-%d')}"
23
+
24
+ file_repo = Repositories::FileStorageRepository.new(base_path, 'fgi.json')
25
+ data = file_repo.load
26
+ return data if data
27
+
28
+ lambda_params = { lambda: { class: 'Clients::Fgi', method: 'values' } }
29
+ json_data = client.invoke(lambda_params.merge(params))
30
+ file_repo.save(json_data)
31
+ json_data
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lunaris
4
+ module Controllers
5
+ class MarketAutomationController
6
+ attr_reader :data
7
+
8
+ def initialize(data)
9
+ @data = data
10
+ end
11
+
12
+ def execute_job
13
+ Jobs::MarketAutomationJob.new(data).perform
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lunaris
4
+ class Endpoint
5
+ attr_reader :logger, :params
6
+
7
+ def initialize(params)
8
+ ENV.fetch('APP_ENV', 'development')
9
+ Config::Application.set_timezone('GMT')
10
+ Config::SentryClient.setup
11
+
12
+ @params = params
13
+ set_datetime
14
+ @logger = Config::Logger.instance
15
+ end
16
+
17
+ def call
18
+ data = params[:data]
19
+ controller = Object.const_get(params[:controller_name]).new(data)
20
+ controller.send(params[:action_name])
21
+ rescue StandardError => e
22
+ logger.error("Error occured: #{e.message}")
23
+ Sentry.capture_exception(e)
24
+ raise e
25
+ end
26
+
27
+ private
28
+
29
+ def set_datetime
30
+ if params&.dig(:data, :timestamps, :end)
31
+ timestamp = params.dig(:data, :timestamps, :end) / 1000
32
+ datetime = Config::Application.timestamp_to_date(timestamp)
33
+ ENV['DATETIME'] = datetime.to_s
34
+ else
35
+ ENV['DATETIME'] = Time.now.to_s
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ostruct'
4
+
5
+ module Lunaris
6
+ module Jobs
7
+ class MarketAutomationJob
8
+ include Lunaris::Helpers::Loggable
9
+
10
+ attr_reader :params
11
+
12
+ def initialize(params = nil)
13
+ @params = params
14
+ @logger = Config::Logger.instance
15
+ end
16
+
17
+ def perform
18
+ cryptos = Models::Crypto.all
19
+ log_info("MarketAutomationJob started with #{cryptos.size} cryptos")
20
+
21
+ futures = create_pipelines(cryptos)
22
+ Concurrent::Promises.zip(*futures).value!
23
+
24
+ log_info('MarketAutomationJob completed successfully')
25
+ end
26
+
27
+ private
28
+
29
+ def create_pipelines(cryptos)
30
+ cryptos.map do |crypto|
31
+ Concurrent::Promises.future do
32
+ context = OpenStruct.new(crypto: crypto, params: params)
33
+ Services::CryptoMarketDataPipeline.new(context).process
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'csv'
4
+
5
+ module Lunaris
6
+ module Models
7
+ class Crypto
8
+ DEFAULT_TICKER_PARAMS = { interval: '1h', limit: 50 }.freeze
9
+ FILE_PATH = 'data/cryptos.csv'
10
+
11
+ attr_reader :name, :symbol
12
+
13
+ def initialize(params)
14
+ @name, @symbol = params.values_at(:name, :symbol)
15
+ end
16
+
17
+ def self.all
18
+ CSV.foreach(FILE_PATH, headers: true).map do |row|
19
+ Models::Crypto.new(name: row['Name'], symbol: row['Symbol'])
20
+ end
21
+ end
22
+
23
+ def tickers(params = nil)
24
+ params = DEFAULT_TICKER_PARAMS.merge(params || {})
25
+ .merge(symbol: "#{symbol}USDT")
26
+
27
+ candles = Clients::Binance.new(params).candlestick
28
+ candles.map { |entry| Models::Ticker.new(entry, self) }
29
+ end
30
+
31
+ def to_h
32
+ {
33
+ crypto: {
34
+ name: name,
35
+ symbol: symbol
36
+ }
37
+ }
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lunaris
4
+ module Models
5
+ class CryptoMarketData
6
+ attr_reader :crypto, :tickers, :indicators
7
+
8
+ def initialize(crypto, tickers, indicators)
9
+ @crypto = crypto
10
+ @tickers = tickers
11
+ @indicators = indicators
12
+ end
13
+
14
+ def to_h
15
+ Builders::CryptoMarketDataBuilder.new(crypto, tickers, indicators).build
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'technical-analysis'
4
+
5
+ module Lunaris
6
+ module Models
7
+ class Indicator
8
+ DATA_PERIOD = {
9
+ adi: 20, atr: 20, bb: 25, cmf: 25,
10
+ ema10: 15, ema20: 25, kc: 15, macd: 39,
11
+ mfi: 20, obv: 20, rsi: 20, sma10: 15,
12
+ sma20: 25, sr: 21, tsi: 43, vwap: 25
13
+ }.freeze
14
+
15
+ attr_reader :cache_data, :indicators, :tickers
16
+
17
+ def initialize(tickers: nil, indicators: nil)
18
+ if tickers
19
+ @tickers = tickers
20
+ @cache_data = {}
21
+ cache_data_by_periods(DATA_PERIOD.values.uniq)
22
+ else
23
+ @indicators = indicators
24
+ end
25
+ end
26
+
27
+ def calculate(type, options = {})
28
+ indicator_class = Object.const_get("TechnicalAnalysis::#{type.capitalize}")
29
+ key = :"#{type}#{options[:period]}"
30
+ period = DATA_PERIOD[type.to_sym] || DATA_PERIOD[key]
31
+
32
+ if %i[adi obv vwap].map(&:to_s).include?(type)
33
+ indicator_class.calculate(cache_data[period])
34
+ else
35
+ indicator_class.calculate(cache_data[period], options)
36
+ end
37
+ end
38
+
39
+ def filter_by_datetime(values, datetime, index = 0)
40
+ unless values.any?(&:date_time)
41
+ all_values = []
42
+
43
+ [values.sma10, values.sma20, values.ema10, values.ema20].each_with_index do |indicator_list, index|
44
+ all_values << filter_by_datetime(indicator_list, datetime, index) if indicator_list&.any?
45
+ end
46
+
47
+ return all_values.flatten
48
+ end
49
+
50
+ filtered_indicators = values.select { |value| value.date_time == datetime }
51
+ [filtered_indicators, index].flatten
52
+ end
53
+
54
+ def filter_by_day(data, ticker_datetime)
55
+ ticker_time = Time.parse(ticker_datetime)
56
+ ticker_timestamp = ticker_time.to_date.to_time.to_i
57
+
58
+ data.select { |d| d.timestamp == ticker_timestamp }
59
+ end
60
+
61
+ private
62
+
63
+ def cache_data_by_periods(periods)
64
+ periods.each { |period| cache_data[period] ||= tickers.last(period) }
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lunaris
4
+ module Models
5
+ class Ticker
6
+ attr_reader :date, :open_price, :close_price, :high_price, :low_price, :volume, :crypto
7
+
8
+ def initialize(candle_params, crypto)
9
+ @date, @open_price, @close_price, @high_price, @low_price, @volume =
10
+ %i[date open_price close_price high_price low_price volume].map { |k| candle_params[k.to_s] }
11
+
12
+ @crypto = crypto
13
+ end
14
+
15
+ def self.sorted_by_date(tickers)
16
+ tickers.sort_by { |ticker| -Time.parse(ticker.date).to_i }
17
+ end
18
+
19
+ def to_h
20
+ {
21
+ date: date,
22
+ open_price: open_price,
23
+ close_price: close_price,
24
+ high_price: high_price,
25
+ low_price: low_price,
26
+ volume: volume
27
+ }
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'fileutils'
5
+
6
+ module Lunaris
7
+ module Repositories
8
+ class FileStorageRepository
9
+ attr_reader :base_directory, :file_name
10
+
11
+ def initialize(base_directory, file_name)
12
+ @base_directory = File.join(Dir.pwd, base_directory)
13
+ @file_name = File.join(@base_directory, file_name)
14
+ @file_name.sub!('data', 'spec/data') if defined?(RSpec)
15
+ end
16
+
17
+ def save(data)
18
+ FileUtils.mkdir_p(File.dirname(file_name))
19
+ File.write(file_name, to_json(data))
20
+ end
21
+
22
+ def load
23
+ return JSON.parse(File.read(file_name)) if File.exist?(file_name)
24
+
25
+ nil
26
+ end
27
+
28
+ private
29
+
30
+ def to_json(data)
31
+ JSON.pretty_generate(data)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lunaris
4
+ module Services
5
+ class CryptoMarketDataPipeline
6
+ include Lunaris::Helpers::FileHelper
7
+ include Lunaris::Helpers::Loggable
8
+
9
+ attr_reader :crypto, :params
10
+
11
+ def initialize(context)
12
+ @crypto = context.crypto
13
+ @params = context.params
14
+ @file_base_path = base_path_hour
15
+ end
16
+
17
+ def process
18
+ log_info("Processing crypto #{crypto.name} ...")
19
+
20
+ tickers = retrieve_tickers
21
+ indicators = calculate_indicators(tickers)
22
+ export_data(tickers, indicators)
23
+
24
+ log_info("Export data for #{crypto.name}")
25
+ end
26
+
27
+ private
28
+
29
+ def calculate_indicators(tickers)
30
+ indicators = Services::IndicatorService.new(tickers).calculate_all
31
+ log_info("Calculation of indicators for #{crypto.name}: OK")
32
+ indicators
33
+ end
34
+
35
+ def export_data(tickers, indicators)
36
+ data = Models::CryptoMarketData.new(crypto, tickers, indicators).to_h
37
+ Repositories::FileStorageRepository.new(@file_base_path, "#{crypto.name}.json").save(data)
38
+ end
39
+
40
+ def retrieve_tickers
41
+ tickers = crypto.tickers(params)
42
+ log_info("Found #{tickers.count} tickers for #{crypto.name}")
43
+ tickers
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lunaris
4
+ module Services
5
+ class IndicatorService
6
+ attr_reader :indicator_obj
7
+
8
+ INDICATOR_CLASSES = {
9
+ trend_following: Indicators::TrendFollowing,
10
+ momentum: Indicators::Momentum,
11
+ volatility: Indicators::Volatility,
12
+ volume_based: Indicators::VolumeBased,
13
+ sentiment: Indicators::Sentiment
14
+ }.freeze
15
+
16
+ def initialize(tickers)
17
+ price_data = load_data(tickers).freeze
18
+ @indicator_obj = Models::Indicator.new(tickers: price_data)
19
+
20
+ initialize_indicators
21
+ end
22
+
23
+ INDICATOR_CLASSES.each_key do |indicator|
24
+ define_method(indicator) do
25
+ instance_variable_get("@#{indicator}").calculate_all
26
+ end
27
+ end
28
+
29
+ def calculate_all
30
+ futures = INDICATOR_CLASSES.keys.map do |indicator|
31
+ Concurrent::Future.execute do
32
+ instance_variable_get("@#{indicator}").calculate_all
33
+ end
34
+ end
35
+
36
+ results = futures.map(&:value).reduce({}) do |acc, struct|
37
+ acc.merge(struct.to_h)
38
+ end
39
+
40
+ OpenStruct.new(results)
41
+ end
42
+
43
+ private
44
+
45
+ def initialize_indicators
46
+ INDICATOR_CLASSES.each do |indicator, klass|
47
+ instance_variable_set("@#{indicator}", klass.new(indicator_obj))
48
+ end
49
+ end
50
+
51
+ def load_data(tickers)
52
+ tickers.map do |t|
53
+ { date_time: t.date, open: t.open_price, high: t.high_price,
54
+ low: t.low_price, close: t.close_price, volume: t.volume }
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lunaris
4
+ module Services
5
+ module Indicators
6
+ class BaseIndicatorService
7
+ attr_reader :indicator_obj
8
+
9
+ def initialize(indicator_obj)
10
+ @indicator_obj = indicator_obj
11
+ end
12
+
13
+ def calculate_all
14
+ results = self.class::OPTIONS.each_with_object({}) do |(type, opts), result|
15
+ result[:"#{type}_values"] = calculate_indicator_for(type, opts)
16
+ end
17
+
18
+ OpenStruct.new(results)
19
+ end
20
+
21
+ private
22
+
23
+ def calculate_indicator_for(type, opts)
24
+ if %i[sma ema].include?(type)
25
+ calculate_moving_average(type)
26
+ else
27
+ calculate_indicator(type, opts)
28
+ end
29
+ end
30
+
31
+ def calculate_moving_average(type)
32
+ OpenStruct.new(
33
+ self.class::OPTIONS[type].transform_values { |opts| calculate_indicator(type, opts) }
34
+ )
35
+ end
36
+
37
+ def calculate_indicator(type, opts = nil)
38
+ indicator_obj.calculate(type.to_s, opts || self.class::OPTIONS[type])
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lunaris
4
+ module Services
5
+ module Indicators
6
+ class Momentum < BaseIndicatorService
7
+ OPTIONS = {
8
+ rsi: { period: 14, price_key: :close },
9
+ sr: {},
10
+ tsi: { low_period: 13, high_period: 25, price_key: :close }
11
+ }.freeze
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'concurrent'
4
+
5
+ module Lunaris
6
+ module Services
7
+ module Indicators
8
+ class Sentiment < BaseIndicatorService
9
+ OPTIONS = {
10
+ adi: { period: 14, price_key: :close },
11
+ mfi: {}
12
+ }.freeze
13
+
14
+ @fgi_semaphore = Concurrent::Semaphore.new(1)
15
+
16
+ def calculate_all
17
+ OpenStruct.new({
18
+ fgi_values: fear_greed_index,
19
+ adi_values: calculate_indicator(:adi),
20
+ mfi_values: calculate_indicator(:mfi)
21
+ })
22
+ end
23
+
24
+ def fear_greed_index(limit = 6)
25
+ params = { limit: limit }
26
+ self.class.fgi_semaphore.acquire
27
+
28
+ begin
29
+ deep_struct(Clients::Fgi.new(params).values)
30
+ ensure
31
+ self.class.fgi_semaphore.release
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def deep_struct(obj)
38
+ case obj
39
+ when Hash
40
+ OpenStruct.new(obj.transform_values { |v| deep_struct(to_i(v)) })
41
+ when Array
42
+ obj.map { |v| deep_struct(v) }
43
+ else
44
+ to_i(obj)
45
+ end
46
+ end
47
+
48
+ def to_i(value)
49
+ value.to_s.match?(/^\d+$/) ? value.to_i : value
50
+ end
51
+
52
+ class << self
53
+ attr_reader :fgi_semaphore
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lunaris
4
+ module Services
5
+ module Indicators
6
+ class TrendFollowing < BaseIndicatorService
7
+ OPTIONS = {
8
+ sma: { 'sma10' => { period: 10, price_key: :close }, 'sma20' => { period: 20, price_key: :close } },
9
+ ema: { 'ema10' => { period: 10, price_key: :close }, 'ema20' => { period: 20, price_key: :close } },
10
+ macd: { fast_period: 12, slow_period: 26, signal_period: 9, price_key: :close }
11
+ }.freeze
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lunaris
4
+ module Services
5
+ module Indicators
6
+ class Volatility < BaseIndicatorService
7
+ OPTIONS = {
8
+ bb: { period: 20, price_key: :close },
9
+ atr: {},
10
+ kc: {}
11
+ }.freeze
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lunaris
4
+ module Services
5
+ module Indicators
6
+ class VolumeBased < BaseIndicatorService
7
+ OPTIONS = {
8
+ obv: { period: 20, price_key: :close },
9
+ cmf: { period: 20 },
10
+ vwap: {}
11
+ }.freeze
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lunaris
4
+ VERSION = '0.0.0'
5
+ end
data/lib/lunaris.rb ADDED
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lunaris/autoloader'
4
+
5
+ module Lunaris
6
+ NAME = 'Lunaris Ruby'
7
+
8
+ @mutex = ::Mutex.new
9
+
10
+ class << self
11
+ def app_info=(value)
12
+ ::Lunaris::Base.app_info = value
13
+ end
14
+
15
+ def ping
16
+ { status: 200 }
17
+ end
18
+
19
+ def version
20
+ VERSION
21
+ end
22
+ end
23
+ end
24
+
25
+ Lunaris::Autoloader.setup!
metadata ADDED
@@ -0,0 +1,242 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lunaris
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Stefano Baldazzi
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-06-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: aws-sdk-lambda
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: concurrent-ruby
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 1.3.5
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 1.3.5
41
+ - !ruby/object:Gem::Dependency
42
+ name: csv
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: file_utils
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: json
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: ostruct
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '0.6'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '0.6'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rest-client
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: sentry-ruby
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: technical-analysis
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: tzinfo
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: 2.0.6
146
+ type: :runtime
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: 2.0.6
153
+ - !ruby/object:Gem::Dependency
154
+ name: vcr
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :runtime
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: zeitwerk
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - "~>"
172
+ - !ruby/object:Gem::Version
173
+ version: '2.4'
174
+ type: :runtime
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - "~>"
179
+ - !ruby/object:Gem::Version
180
+ version: '2.4'
181
+ description: Lunaris is a lightweight gem for manage crypto operations.
182
+ email:
183
+ - stefanobaldazzi40@gmail.com
184
+ executables: []
185
+ extensions: []
186
+ extra_rdoc_files: []
187
+ files:
188
+ - README.md
189
+ - lib/lunaris.rb
190
+ - lib/lunaris/autoloader.rb
191
+ - lib/lunaris/base.rb
192
+ - lib/lunaris/config/application.rb
193
+ - lib/lunaris/config/logger.rb
194
+ - lib/lunaris/config/sentry_client.rb
195
+ - lib/lunaris/helpers/file_helper.rb
196
+ - lib/lunaris/helpers/loggable.rb
197
+ - lib/lunaris/resources/builders/crypto_market_data_builder.rb
198
+ - lib/lunaris/resources/clients/aws.rb
199
+ - lib/lunaris/resources/clients/binance.rb
200
+ - lib/lunaris/resources/clients/fgi.rb
201
+ - lib/lunaris/resources/controllers/market_automation_controller.rb
202
+ - lib/lunaris/resources/endpoint.rb
203
+ - lib/lunaris/resources/jobs/market_automation_job.rb
204
+ - lib/lunaris/resources/models/crypto.rb
205
+ - lib/lunaris/resources/models/crypto_market_data.rb
206
+ - lib/lunaris/resources/models/indicator.rb
207
+ - lib/lunaris/resources/models/ticker.rb
208
+ - lib/lunaris/resources/repositories/file_storage_repository.rb
209
+ - lib/lunaris/resources/services/crypto_market_data_pipeline.rb
210
+ - lib/lunaris/resources/services/indicator_service.rb
211
+ - lib/lunaris/resources/services/indicators/base_indicator_service.rb
212
+ - lib/lunaris/resources/services/indicators/momentum.rb
213
+ - lib/lunaris/resources/services/indicators/sentiment.rb
214
+ - lib/lunaris/resources/services/indicators/trend_following.rb
215
+ - lib/lunaris/resources/services/indicators/volatility.rb
216
+ - lib/lunaris/resources/services/indicators/volume_based.rb
217
+ - lib/lunaris/version.rb
218
+ homepage: https://github.com/Baldaz02/next-gen-ruby
219
+ licenses:
220
+ - MIT
221
+ metadata:
222
+ rubygems_mfa_required: 'true'
223
+ post_install_message:
224
+ rdoc_options: []
225
+ require_paths:
226
+ - lib
227
+ required_ruby_version: !ruby/object:Gem::Requirement
228
+ requirements:
229
+ - - ">="
230
+ - !ruby/object:Gem::Version
231
+ version: '2.7'
232
+ required_rubygems_version: !ruby/object:Gem::Requirement
233
+ requirements:
234
+ - - ">="
235
+ - !ruby/object:Gem::Version
236
+ version: '0'
237
+ requirements: []
238
+ rubygems_version: 3.2.22
239
+ signing_key:
240
+ specification_version: 4
241
+ summary: lunaris-0.0.0
242
+ test_files: []