tradingview-screener 0.2.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +18 -0
- data/LICENSE.txt +21 -0
- data/README.md +179 -0
- data/lib/tradingview/screener.rb +4 -0
- data/lib/tradingview_screener/base.rb +291 -0
- data/lib/tradingview_screener/client.rb +206 -0
- data/lib/tradingview_screener/predicate.rb +107 -0
- data/lib/tradingview_screener/relation.rb +581 -0
- data/lib/tradingview_screener/screen_data.rb +343 -0
- data/lib/tradingview_screener/version.rb +5 -0
- data/lib/tradingview_screener.rb +31 -0
- metadata +116 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module TradingviewScreener
|
|
8
|
+
# Convert TradingView saved-screener `window.initData.screen_data`
|
|
9
|
+
# into scanner.tradingview.com scan payloads / Relations.
|
|
10
|
+
module ScreenData
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def to_payload(screen_data)
|
|
14
|
+
Converter.new(screen_data).to_payload
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_relation(screen_data, model: Stock)
|
|
18
|
+
model.from_payload(to_payload(screen_data))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def parse_html(html)
|
|
22
|
+
Parser.extract_screen_data(html)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def fetch(url, cookies: nil, proxy: nil, timeout: 20, headers: nil)
|
|
26
|
+
Fetcher.new(cookies: cookies, proxy: proxy, timeout: timeout, headers: headers).fetch(url)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class ParseError < Error; end
|
|
30
|
+
class FetchError < Error; end
|
|
31
|
+
|
|
32
|
+
class Parser
|
|
33
|
+
SCREEN_DATA_RE = /window\.initData\.screen_data\s*=\s*(\{.*?\})\s*;\s*\n\s*window\.initData\.onScreenerStandalonePage/m
|
|
34
|
+
|
|
35
|
+
def self.extract_screen_data(html)
|
|
36
|
+
text = html.to_s
|
|
37
|
+
match = text.match(SCREEN_DATA_RE)
|
|
38
|
+
raise ParseError, "screen_data not found in HTML" unless match
|
|
39
|
+
|
|
40
|
+
JSON.parse(match[1])
|
|
41
|
+
rescue JSON::ParserError => e
|
|
42
|
+
raise ParseError, "invalid screen_data JSON: #{e.message}"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
class Fetcher
|
|
47
|
+
def initialize(cookies: nil, proxy: nil, timeout: 20, headers: nil)
|
|
48
|
+
@cookies = cookies
|
|
49
|
+
@proxy = proxy
|
|
50
|
+
@timeout = timeout
|
|
51
|
+
@headers = headers
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def fetch(url)
|
|
55
|
+
client = Client.new(
|
|
56
|
+
timeout: @timeout,
|
|
57
|
+
cookies: @cookies,
|
|
58
|
+
proxy: @proxy,
|
|
59
|
+
headers: {
|
|
60
|
+
"accept" => "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
61
|
+
"user-agent" => "tradingview-screener-rb/#{VERSION}"
|
|
62
|
+
}.merge(@headers || {})
|
|
63
|
+
)
|
|
64
|
+
html = client.get(url)
|
|
65
|
+
data = Parser.extract_screen_data(html)
|
|
66
|
+
{
|
|
67
|
+
"url" => url,
|
|
68
|
+
"screen_data" => data,
|
|
69
|
+
"payload" => Converter.new(data).to_payload
|
|
70
|
+
}
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
class Converter
|
|
75
|
+
COLUMN_MAP = {
|
|
76
|
+
"Price" => "close",
|
|
77
|
+
"Change" => "change",
|
|
78
|
+
"Volume" => "volume",
|
|
79
|
+
"MarketCap" => "market_cap_basic",
|
|
80
|
+
"PriceToEarnings" => "price_earnings_ttm",
|
|
81
|
+
"EpsDiluted" => "earnings_per_share_diluted_ttm",
|
|
82
|
+
"EpsDilutedGrowth" => "earnings_per_share_diluted_yoy_growth_ttm",
|
|
83
|
+
"DividendsYield" => "dividends_yield_current",
|
|
84
|
+
"Sector" => "sector",
|
|
85
|
+
"AnalystRating" => "AnalystRating",
|
|
86
|
+
"PreMarketChange" => "premarket_change",
|
|
87
|
+
"PreMarketVolume" => "premarket_volume",
|
|
88
|
+
"PreMarketGap" => "premarket_gap",
|
|
89
|
+
"TickerUniversal" => "ticker-view",
|
|
90
|
+
"RelativeVolume" => "relative_volume_10d_calc",
|
|
91
|
+
"RelativeVolumeAtTime" => "relative_volume_intraday|5",
|
|
92
|
+
"AverageTrueRangePercent" => "ATRP",
|
|
93
|
+
"PriceAvgVolume" => "AvgValue.Traded_10d",
|
|
94
|
+
"AverageVolume" => "average_volume_10d_calc",
|
|
95
|
+
"Performance" => "Perf.W",
|
|
96
|
+
"RevenueGrowth" => "total_revenue_yoy_growth_ttm",
|
|
97
|
+
"PriceToEarningsToGrowth" => "price_earnings_growth_ttm",
|
|
98
|
+
"ReturnOnEquity" => "return_on_equity",
|
|
99
|
+
"Beta" => "beta_1_year",
|
|
100
|
+
"Ma" => "SMA"
|
|
101
|
+
}.freeze
|
|
102
|
+
|
|
103
|
+
SORT_MAP = {
|
|
104
|
+
"PreMarketVolume" => "premarket_volume",
|
|
105
|
+
"MarketCap" => "market_cap_basic",
|
|
106
|
+
"Volume" => "volume",
|
|
107
|
+
"Change" => "change",
|
|
108
|
+
"Price" => "close",
|
|
109
|
+
"PreMarketChange" => "premarket_change",
|
|
110
|
+
"RelativeVolume" => "relative_volume_10d_calc"
|
|
111
|
+
}.freeze
|
|
112
|
+
|
|
113
|
+
DEFAULT_COLUMNS = %w[
|
|
114
|
+
ticker-view close type typespecs pricescale minmov fractional minmove2 currency
|
|
115
|
+
change volume relative_volume_10d_calc market_cap_basic fundamental_currency_code
|
|
116
|
+
price_earnings_ttm earnings_per_share_diluted_ttm earnings_per_share_diluted_yoy_growth_ttm
|
|
117
|
+
dividends_yield_current sector.tr market sector AnalystRating AnalystRating.tr
|
|
118
|
+
premarket_change premarket_volume
|
|
119
|
+
].freeze
|
|
120
|
+
|
|
121
|
+
def initialize(screen_data)
|
|
122
|
+
@data = deep_stringify(screen_data)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def to_payload
|
|
126
|
+
markets = Array(@data.dig("market_settings", "markets"))
|
|
127
|
+
markets = ["america"] if markets.empty?
|
|
128
|
+
filters = convert_filters(Array(@data["filters"]))
|
|
129
|
+
if @data.dig("market_settings", "is_primary_listing")
|
|
130
|
+
filters << { "left" => "is_primary", "operation" => "equal", "right" => true }
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
{
|
|
134
|
+
"markets" => markets,
|
|
135
|
+
"symbols" => {},
|
|
136
|
+
"options" => { "lang" => "en" },
|
|
137
|
+
"columns" => convert_columns(Array(@data["default_custom_column_set"])),
|
|
138
|
+
"filter" => filters,
|
|
139
|
+
"filter2" => deep_dup(Relation::STOCK_TYPE_FILTER2),
|
|
140
|
+
"sort" => convert_sort,
|
|
141
|
+
"range" => [0, 100],
|
|
142
|
+
"ignore_unknown_fields" => false,
|
|
143
|
+
"meta" => {
|
|
144
|
+
"id" => @data["id"],
|
|
145
|
+
"title" => @data["title"],
|
|
146
|
+
"version" => @data["version"],
|
|
147
|
+
"date_created" => @data["date_created"],
|
|
148
|
+
"date_updated" => @data["date_updated"],
|
|
149
|
+
"date_used" => @data["date_used"]
|
|
150
|
+
}.compact
|
|
151
|
+
}
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
private
|
|
155
|
+
|
|
156
|
+
# Keep screen-defined columns, but always include the fields used by trading-universe metadata.
|
|
157
|
+
REQUIRED_METADATA_COLUMNS = %w[
|
|
158
|
+
close
|
|
159
|
+
change
|
|
160
|
+
volume
|
|
161
|
+
relative_volume_10d_calc
|
|
162
|
+
sector
|
|
163
|
+
AnalystRating
|
|
164
|
+
AnalystRating.tr
|
|
165
|
+
premarket_change
|
|
166
|
+
].freeze
|
|
167
|
+
|
|
168
|
+
def convert_columns(column_set)
|
|
169
|
+
cols = column_set.filter_map { |item| map_column_ref(item) }
|
|
170
|
+
cols = DEFAULT_COLUMNS.dup if cols.empty?
|
|
171
|
+
# Ensure type fields used by filter2 compatibility remain present,
|
|
172
|
+
# and always request the metadata columns consumers expect.
|
|
173
|
+
(%w[type typespecs] + cols + REQUIRED_METADATA_COLUMNS).uniq
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def deep_dup(value)
|
|
177
|
+
case value
|
|
178
|
+
when Hash then value.each_with_object({}) { |(k, v), memo| memo[k] = deep_dup(v) }
|
|
179
|
+
when Array then value.map { |item| deep_dup(item) }
|
|
180
|
+
when String then value.dup
|
|
181
|
+
else value
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def convert_sort
|
|
186
|
+
column = @data.dig("sort_column", "id").to_s
|
|
187
|
+
direction = @data["sort_direction"].to_s.downcase == "asc" ? "asc" : "desc"
|
|
188
|
+
sort_by = SORT_MAP[column] || map_column_id(column, @data.dig("sort_column", "params") || {}) || "market_cap_basic"
|
|
189
|
+
{
|
|
190
|
+
"sortBy" => sort_by,
|
|
191
|
+
"sortOrder" => direction
|
|
192
|
+
}
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def convert_filters(filters)
|
|
196
|
+
filters.filter_map { |filter| convert_filter(filter) }
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def convert_filter(filter)
|
|
200
|
+
type = filter["type"].to_s
|
|
201
|
+
case type
|
|
202
|
+
when "Condition"
|
|
203
|
+
convert_condition(filter)
|
|
204
|
+
when "CheckboxGroup"
|
|
205
|
+
convert_checkbox_group(filter)
|
|
206
|
+
when "Date"
|
|
207
|
+
nil # relative date filters with null are inactive placeholders
|
|
208
|
+
else
|
|
209
|
+
nil
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def convert_condition(filter)
|
|
214
|
+
left = map_column_ref(filter.dig("left", "column"))
|
|
215
|
+
return nil if left.nil? || left == ""
|
|
216
|
+
|
|
217
|
+
operation = filter.dig("operation", "type").to_s
|
|
218
|
+
right = filter["right"]
|
|
219
|
+
target = filter["target"].to_s
|
|
220
|
+
|
|
221
|
+
if right.is_a?(Hash) && right.key?("column")
|
|
222
|
+
right_col = map_column_ref(right["column"])
|
|
223
|
+
return nil if right_col.nil? || right_col == ""
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
"left" => left,
|
|
227
|
+
"operation" => operation_name(operation, equal_ok: true),
|
|
228
|
+
"right" => right_col
|
|
229
|
+
}
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
if right.is_a?(Hash) && (right.key?("left") || right.key?("right")) && operation == "between"
|
|
233
|
+
low = right["left"]
|
|
234
|
+
high = right["right"]
|
|
235
|
+
return nil if low.nil? && high.nil?
|
|
236
|
+
|
|
237
|
+
if !low.nil? && !high.nil?
|
|
238
|
+
return { "left" => left, "operation" => "in_range", "right" => [low, high] }
|
|
239
|
+
elsif !low.nil?
|
|
240
|
+
return { "left" => left, "operation" => "egreater", "right" => low }
|
|
241
|
+
else
|
|
242
|
+
return { "left" => left, "operation" => "eless", "right" => high }
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
value = right.is_a?(Hash) ? right["value"] : right
|
|
247
|
+
return nil if value.nil? && target == "value"
|
|
248
|
+
|
|
249
|
+
{
|
|
250
|
+
"left" => left,
|
|
251
|
+
"operation" => operation_name(operation, equal_ok: true),
|
|
252
|
+
"right" => value
|
|
253
|
+
}
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def convert_checkbox_group(filter)
|
|
257
|
+
values = filter.dig("right", "values")
|
|
258
|
+
return nil if values.nil? || Array(values).empty?
|
|
259
|
+
|
|
260
|
+
left = map_column_ref(filter.dig("left", "column"))
|
|
261
|
+
return nil if left.nil? || left == ""
|
|
262
|
+
|
|
263
|
+
{
|
|
264
|
+
"left" => left,
|
|
265
|
+
"operation" => "in_range",
|
|
266
|
+
"right" => Array(values)
|
|
267
|
+
}
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def operation_name(type, equal_ok: false)
|
|
271
|
+
case type.to_s
|
|
272
|
+
when "above", "greater" then "greater"
|
|
273
|
+
when "below", "less" then "less"
|
|
274
|
+
when "above_or_equal", "egreater" then "egreater"
|
|
275
|
+
when "below_or_equal", "eless" then "eless"
|
|
276
|
+
when "equal", "eq" then "equal"
|
|
277
|
+
when "not_equal", "nequal" then "nequal"
|
|
278
|
+
when "between" then "in_range"
|
|
279
|
+
else
|
|
280
|
+
equal_ok ? "equal" : type.to_s
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def map_column_ref(column)
|
|
285
|
+
return nil if column.nil? || column == {} || column == ""
|
|
286
|
+
|
|
287
|
+
map_column_id(column["id"], column["params"] || {})
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def map_column_id(id, params)
|
|
291
|
+
id = id.to_s
|
|
292
|
+
params = deep_stringify(params || {})
|
|
293
|
+
|
|
294
|
+
case id
|
|
295
|
+
when "Ma"
|
|
296
|
+
length = params["length"]
|
|
297
|
+
length = "20" if length.nil? || length == ""
|
|
298
|
+
"SMA#{length}"
|
|
299
|
+
when "AverageTrueRangePercent"
|
|
300
|
+
"ATRP"
|
|
301
|
+
when "RelativeVolumeAtTime"
|
|
302
|
+
# default TV intraday relative volume used by saved screens
|
|
303
|
+
"relative_volume_intraday|5"
|
|
304
|
+
when "RelativeVolume"
|
|
305
|
+
"relative_volume_10d_calc"
|
|
306
|
+
when "PriceAvgVolume"
|
|
307
|
+
"AvgValue.Traded_10d"
|
|
308
|
+
when "AverageVolume"
|
|
309
|
+
interval = params["interval"].to_s
|
|
310
|
+
case interval
|
|
311
|
+
when "Interval10D" then "average_volume_10d_calc"
|
|
312
|
+
when "Interval30D" then "average_volume_30d_calc"
|
|
313
|
+
else "average_volume_10d_calc"
|
|
314
|
+
end
|
|
315
|
+
when "Performance"
|
|
316
|
+
interval = params["interval"].to_s
|
|
317
|
+
case interval
|
|
318
|
+
when "Interval1W" then "Perf.W"
|
|
319
|
+
when "Interval1M" then "Perf.1M"
|
|
320
|
+
when "Interval3M" then "Perf.3M"
|
|
321
|
+
when "Interval6M" then "Perf.6M"
|
|
322
|
+
when "IntervalYTD" then "Perf.YTD"
|
|
323
|
+
when "Interval1Y" then "Perf.Y"
|
|
324
|
+
else "Perf.W"
|
|
325
|
+
end
|
|
326
|
+
else
|
|
327
|
+
COLUMN_MAP[id] || id
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def deep_stringify(value)
|
|
332
|
+
case value
|
|
333
|
+
when Hash
|
|
334
|
+
value.each_with_object({}) { |(k, v), memo| memo[k.to_s] = deep_stringify(v) }
|
|
335
|
+
when Array
|
|
336
|
+
value.map { |item| deep_stringify(item) }
|
|
337
|
+
else
|
|
338
|
+
value
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "tradingview_screener/version"
|
|
4
|
+
require_relative "tradingview_screener/predicate"
|
|
5
|
+
require_relative "tradingview_screener/client"
|
|
6
|
+
require_relative "tradingview_screener/relation"
|
|
7
|
+
require_relative "tradingview_screener/screen_data"
|
|
8
|
+
require_relative "tradingview_screener/base"
|
|
9
|
+
|
|
10
|
+
module TradingviewScreener
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
# Field helper: TradingviewScreener[:close]
|
|
14
|
+
def [](name)
|
|
15
|
+
Field.new(name)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Include predicate helpers into Relation instances for scope blocks.
|
|
19
|
+
module QueryMethods
|
|
20
|
+
def gt(value) = Predicates.gt(value)
|
|
21
|
+
def gte(value) = Predicates.gte(value)
|
|
22
|
+
def lt(value) = Predicates.lt(value)
|
|
23
|
+
def lte(value) = Predicates.lte(value)
|
|
24
|
+
def not_eq(value) = Predicates.not_eq(value)
|
|
25
|
+
def has(value) = Predicates.has(value)
|
|
26
|
+
def has_none_of(value) = Predicates.has_none_of(value)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
Relation.include(QueryMethods)
|
|
30
|
+
Base.extend(QueryMethods)
|
|
31
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: tradingview-screener
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.2.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Victor Yang
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rspec
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '3.13'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '3.13'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: webmock
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '3.23'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '3.23'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: socksify
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '1.7'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '1.7'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rake
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '13.0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '13.0'
|
|
68
|
+
description: |
|
|
69
|
+
Chainable Query/Column API for TradingView's scanner endpoints.
|
|
70
|
+
Inspired by shner-elmo/TradingView-Screener, designed like Rails ORM relations.
|
|
71
|
+
email:
|
|
72
|
+
- ootyoungtoo@gmail.com
|
|
73
|
+
executables: []
|
|
74
|
+
extensions: []
|
|
75
|
+
extra_rdoc_files: []
|
|
76
|
+
files:
|
|
77
|
+
- CHANGELOG.md
|
|
78
|
+
- LICENSE.txt
|
|
79
|
+
- README.md
|
|
80
|
+
- lib/tradingview/screener.rb
|
|
81
|
+
- lib/tradingview_screener.rb
|
|
82
|
+
- lib/tradingview_screener/base.rb
|
|
83
|
+
- lib/tradingview_screener/client.rb
|
|
84
|
+
- lib/tradingview_screener/predicate.rb
|
|
85
|
+
- lib/tradingview_screener/relation.rb
|
|
86
|
+
- lib/tradingview_screener/screen_data.rb
|
|
87
|
+
- lib/tradingview_screener/version.rb
|
|
88
|
+
homepage: https://rubygems.org/gems/tradingview-screener
|
|
89
|
+
licenses:
|
|
90
|
+
- MIT
|
|
91
|
+
metadata:
|
|
92
|
+
homepage_uri: https://rubygems.org/gems/tradingview-screener
|
|
93
|
+
source_code_uri: https://github.com/Youngv/tradingview-screener
|
|
94
|
+
changelog_uri: https://github.com/Youngv/tradingview-screener/blob/main/CHANGELOG.md
|
|
95
|
+
bug_tracker_uri: https://github.com/Youngv/tradingview-screener/issues
|
|
96
|
+
documentation_uri: https://www.rubydoc.info/gems/tradingview-screener
|
|
97
|
+
allowed_push_host: https://rubygems.org
|
|
98
|
+
rubygems_mfa_required: 'true'
|
|
99
|
+
rdoc_options: []
|
|
100
|
+
require_paths:
|
|
101
|
+
- lib
|
|
102
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
103
|
+
requirements:
|
|
104
|
+
- - ">="
|
|
105
|
+
- !ruby/object:Gem::Version
|
|
106
|
+
version: 3.1.0
|
|
107
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
108
|
+
requirements:
|
|
109
|
+
- - ">="
|
|
110
|
+
- !ruby/object:Gem::Version
|
|
111
|
+
version: '0'
|
|
112
|
+
requirements: []
|
|
113
|
+
rubygems_version: 3.6.9
|
|
114
|
+
specification_version: 4
|
|
115
|
+
summary: ActiveRecord-style Ruby client for TradingView scanner/screener API
|
|
116
|
+
test_files: []
|