clickclient_scrap 0.1.7

Sign up to get free protection for your applications and to get access to all the features.
data/ChangeLog ADDED
File without changes
data/README ADDED
@@ -0,0 +1,28 @@
1
+
2
+ ==クリック証券Fxクライアントライブラリ
3
+
4
+ クリック証券の携帯向けサイト「モバトレ君」にアクセスし、
5
+ スクレイピングでレート情報の取得や取り引きを行うライブラリです。
6
+
7
+ ===実装済みの機能
8
+ - ログイン
9
+ - ログアウト
10
+ - レート一覧の取得
11
+ - 注文一覧の取得
12
+ - 建玉一覧の取得
13
+ - 注文
14
+ -- 成行
15
+ -- 指値
16
+ -- OCO
17
+ - 注文のキャンセル
18
+ - 決済
19
+ -- 成行
20
+ - 余力情報取得
21
+
22
+ ===注意事項
23
+ - スクレイピングによるアクセスのため、Webサイトの仕様変更等により
24
+ 突然動作しなくなる可能性があります。
25
+
26
+ ===免責
27
+ - 本ライブラリの利用は自己責任でお願いします。
28
+ - ライブラリの不備・不具合等によるあらゆる損害について、作成者は責任を負いません。
@@ -0,0 +1,672 @@
1
+ begin
2
+ require 'rubygems'
3
+ rescue LoadError
4
+ end
5
+ require 'mechanize'
6
+ require 'date'
7
+ require 'kconv'
8
+ require 'set'
9
+
10
+ #
11
+ #=== クリック証券アクセスクライアント
12
+ #
13
+ #*Version*:: -
14
+ #*License*:: Ruby ライセンスに準拠
15
+ #
16
+ #クリック証券を利用するためのクライアントライブラリです。携帯向けサイトのスクレイピングにより以下の機能を提供します。
17
+ #- 外為証拠金取引(FX)取引
18
+ #
19
+ #====基本的な使い方
20
+ #
21
+ # require 'clickclient_scrap'
22
+ #
23
+ # c = ClickClient::Client.new
24
+ # # c = ClickClientScrap::Client.new https://<プロキシホスト>:<プロキシポート> # プロキシを利用する場合
25
+ # c.fx_session( "<ユーザー名>", "<パスワード>" ) { | fx_session |
26
+ # # 通貨ペア一覧取得
27
+ # list = fx_session.list_rates
28
+ # puts list
29
+ # }
30
+ #
31
+ #====免責
32
+ #- 本ライブラリの利用は自己責任でお願いします。
33
+ #- ライブラリの不備・不具合等によるあらゆる損害について、作成者は責任を負いません。
34
+ #
35
+ module ClickClientScrap
36
+
37
+ # クライアント
38
+ class Client
39
+ # ホスト名
40
+ DEFAULT_HOST_NAME = "https://sec-sso.click-sec.com/mf/"
41
+
42
+ #
43
+ #===コンストラクタ
44
+ #
45
+ #*proxy*:: プロキシホストを利用する場合、そのホスト名とパスを指定します。
46
+ # 例) https://proxyhost.com:80
47
+ #
48
+ def initialize( proxy=nil )
49
+ @client = WWW::Mechanize.new {|c|
50
+ # プロキシ
51
+ if proxy
52
+ uri = URI.parse( proxy )
53
+ c.set_proxy( uri.host, uri.port )
54
+ end
55
+ }
56
+ @client.keep_alive = false
57
+ @client.max_history=0
58
+ @client.user_agent_alias = 'Windows IE 7'
59
+ @host_name = DEFAULT_HOST_NAME
60
+ end
61
+
62
+ #ログインし、セッションを開始します。
63
+ #-ブロックを指定した場合、引数としてセッションを指定してブロックを実行します。ブロック実行後、ログアウトします。
64
+ #-そうでない場合、セッションを返却します。この場合、ClickClientScrap::FX::FxSession#logoutを実行しログアウトしてください。
65
+ #
66
+ #userid:: ユーザーID
67
+ #password:: パスワード
68
+ #options:: オプション
69
+ #戻り値:: ClickClientScrap::FX::FxSession
70
+ def fx_session( userid, password, options={}, &block )
71
+ page = @client.get(@host_name)
72
+ ClickClientScrap::Client.error(page) if page.forms.length <= 0
73
+ form = page.forms.first
74
+ form.j_username = userid
75
+ form.j_password = password
76
+ result = @client.submit(form, form.buttons.first)
77
+ if result.body.toutf8 =~ /<META HTTP-EQUIV="REFRESH" CONTENT="0;URL=([^"]*)">/
78
+ result = @client.get($1)
79
+ ClickClientScrap::Client.error( result ) if result.links.size <= 0
80
+ session = FX::FxSession.new( @client, result.links, options )
81
+ if block_given?
82
+ begin
83
+ yield session
84
+ ensure
85
+ session.logout
86
+ end
87
+ else
88
+ return session
89
+ end
90
+ else
91
+ ClickClientScrap::Client.error( result )
92
+ end
93
+ end
94
+ def self.error( page )
95
+ msgs = page.body.scan( /<font color="red">([^<]*)</ ).flatten
96
+ error = !msgs.empty? ? msgs.map{|m| m.strip}.join(",") : page.body
97
+ raise "operation failed.detail=#{error}".toutf8
98
+ end
99
+
100
+ #ホスト名
101
+ attr :host_name, true
102
+ end
103
+
104
+ module FX
105
+
106
+ # 通貨ペア: 米ドル-円
107
+ USDJPY = :USDJPY
108
+ # 通貨ペア: ユーロ-円
109
+ EURJPY = :EURJPY
110
+ # 通貨ペア: イギリスポンド-円
111
+ GBPJPY = :GBPJPY
112
+ # 通貨ペア: 豪ドル-円
113
+ AUDJPY = :AUDJPY
114
+ # 通貨ペア: ニュージーランドドル-円
115
+ NZDJPY = :NZDJPY
116
+ # 通貨ペア: カナダドル-円
117
+ CADJPY = :CADJPY
118
+ # 通貨ペア: スイスフラン-円
119
+ CHFJPY = :CHFJPY
120
+ # 通貨ペア: 南アランド-円
121
+ ZARJPY = :ZARJPY
122
+ # 通貨ペア: ユーロ-米ドル
123
+ EURUSD = :EURUSD
124
+ # 通貨ペア: イギリスポンド-米ドル
125
+ GBPUSD = :GBPUSD
126
+ # 通貨ペア: 豪ドル-米ドル
127
+ AUDUSD = :AUDUSD
128
+ # 通貨ペア: ユーロ-スイスフラン
129
+ EURCHF = :EURCHF
130
+ # 通貨ペア: イギリスポンド-スイスフラン
131
+ GBPCHF = :GBPCHF
132
+ # 通貨ペア: 米ドル-スイスフラン
133
+ USDCHF = :USDCHF
134
+
135
+ # 売買区分: 買い
136
+ BUY = 0
137
+ # 売買区分: 売り
138
+ SELL = 1
139
+
140
+ # 注文タイプ: 通常
141
+ ORDER_TYPE_MARKET_ORDER = "00"
142
+ # 注文タイプ: 通常
143
+ ORDER_TYPE_NORMAL = "01"
144
+ # 注文タイプ: IFD
145
+ ORDER_TYPE_IFD = "11"
146
+ # 注文タイプ: OCO
147
+ ORDER_TYPE_OCO = "21"
148
+ # 注文タイプ: IFD-OCO
149
+ ORDER_TYPE_IFD_OCO = "31"
150
+
151
+ # 有効期限: 当日限り
152
+ EXPIRATION_TYPE_TODAY = 0
153
+ # 有効期限: 週末まで
154
+ EXPIRATION_TYPE_WEEK_END = 1
155
+ # 有効期限: 無期限
156
+ EXPIRATION_TYPE_INFINITY = 2
157
+ # 有効期限: 日付指定
158
+ EXPIRATION_TYPE_SPECIFIED = 3
159
+
160
+ # 注文状況: すべて
161
+ ORDER_CONDITION_ALL = ""
162
+ # 注文状況: 注文中
163
+ ORDER_CONDITION_ON_ORDER = "0"
164
+ # 注文状況: 取消済
165
+ ORDER_CONDITION_CANCELED = "1"
166
+ # 注文状況: 約定
167
+ ORDER_CONDITION_EXECUTION = "2"
168
+ # 注文状況: 不成立
169
+ ORDER_CONDITION_FAILED = "3"
170
+
171
+ # トレード種別: 新規
172
+ TRADE_TYPE_NEW = "新規"
173
+ # トレード種別: 決済
174
+ TRADE_TYPE_SETTLEMENT = "決済"
175
+
176
+ # 執行条件: 成行
177
+ EXECUTION_EXPRESSION_MARKET_ORDER = "成行"
178
+ # 執行条件: 指値
179
+ EXECUTION_EXPRESSION_LIMIT_ORDER = "指値"
180
+ # 執行条件: 逆指値
181
+ EXECUTION_EXPRESSION_REVERSE_LIMIT_ORDER = "逆指値"
182
+
183
+ #=== FX取引のためのセッションクラス
184
+ #Client#fx_sessionのブロックの引数として渡されます。詳細はClient#fx_sessionを参照ください。
185
+ class FxSession
186
+
187
+ def initialize( client, links, options={} )
188
+ @client = client
189
+ @links = links
190
+ @options = options
191
+ end
192
+
193
+ #レート一覧を取得します。
194
+ #
195
+ #戻り値:: 通貨ペアをキーとするClickClientScrap::FX::Rateのハッシュ。
196
+ def list_rates
197
+ result = link_click( "1" )
198
+ if !@last_update_time_of_swaps \
199
+ || Time.now.to_i - @last_update_time_of_swaps > (@options[:swap_update_interval] || 60*60)
200
+ @swaps = list_swaps
201
+ @last_update_time_of_swaps = Time.now.to_i
202
+ end
203
+ reg = />([A-Z]+\/[A-Z]+)<\/a>[^\-\.\d]*?([\d]+\.[\d]+\-[\d]+)/
204
+ tokens = result.body.toutf8.scan( reg )
205
+ ClickClientScrap::Client.error( result ) if !tokens || tokens.empty?
206
+ return tokens.inject({}) {|r,l|
207
+ pair = to_pair( l[0] )
208
+ swap = @swaps[pair]
209
+ rate = FxSession.convert_rate l[1]
210
+ if ( rate && swap )
211
+ r[pair] = Rate.new( pair, rate[0], rate[1], swap.sell_swap, swap.buy_swap )
212
+ end
213
+ r
214
+ }
215
+ end
216
+ #12.34-35 形式の文字列をbidレート、askレートに変換する。
217
+ def self.convert_rate( str ) #:nodoc:
218
+ if str =~ /([\d]+)\.([\d]+)\-([\d]+)/
219
+ high = $1
220
+ low = $2
221
+ low2 = $3
222
+ bid = high.to_f+(low.to_f/(10**low.length))
223
+ ask_low = (low[0...low.length-low2.length] + low2).to_f
224
+ if low.to_f > ask_low
225
+ ask_low += 10**low2.length
226
+ end
227
+ ask = high.to_f+(ask_low/10**low.length)
228
+ return [bid,ask]
229
+ end
230
+ end
231
+
232
+ #スワップの一覧を取得します。
233
+ #
234
+ #戻り値:: 通貨ペアをキーとするClickClientScrap::FX::Swapのハッシュ。
235
+ def list_swaps
236
+ result = link_click( "8" )
237
+ reg = /<dd>([A-Z]+\/[A-Z]+) <font[^>]*>売<\/font>[^\-\d]*?([\-\d]+)[^\-\d]*<font[^>]*>買<\/font>[^\-\d]*([\-\d]+)[^\-\d]*<\/dd>/
238
+ return result.body.toutf8.scan( reg ).inject({}) {|r,l|
239
+ pair = to_pair( l[0] )
240
+ r[pair] = Swap.new( pair, l[1].to_i, l[2].to_i ); r
241
+ }
242
+ end
243
+
244
+ #
245
+ #注文を行います。
246
+ #
247
+ #currency_pair_code:: 通貨ペアコード(必須)
248
+ #sell_or_buy:: 売買区分。ClickClientScrap::FX::BUY,ClickClientScrap::FX::SELLのいずれかを指定します。(必須)
249
+ #unit:: 取引数量(必須)
250
+ #options:: 注文のオプション。注文方法に応じて以下の情報を設定できます。
251
+ # - <b>成り行き注文</b>
252
+ # - <tt>:slippage</tt> .. スリッページ (オプション)。何pips以内かを整数で指定します。
253
+ # - <b>通常注文</b> ※注文レートが設定されていれば通常取引となります。
254
+ # - <tt>:rate</tt> .. 注文レート(必須)
255
+ # - <tt>:execution_expression</tt> .. 執行条件。ClickClientScrap::FX::EXECUTION_EXPRESSION_LIMIT_ORDER等を指定します(必須)
256
+ # - <tt>:expiration_type</tt> .. 有効期限。ClickClientScrap::FX::EXPIRATION_TYPE_TODAY等を指定します(必須)
257
+ # - <tt>:expiration_date</tt> .. 有効期限が「日付指定(ClickClientScrap::FX::EXPIRATION_TYPE_SPECIFIED)」の場合の有効期限をDateで指定します。(有効期限が「日付指定」の場合、必須)
258
+ # - <b>OCO注文</b> ※逆指値レートが設定されていればOCO取引となります。
259
+ # - <tt>:rate</tt> .. 注文レート(必須)
260
+ # - <tt>:stop_order_rate</tt> .. 逆指値レート(必須)
261
+ # - <tt>:expiration_type</tt> .. 有効期限。ClickClientScrap::FX::EXPIRATION_TYPE_TODAY等を指定します(必須)
262
+ # - <tt>:expiration_date</tt> .. 有効期限が「日付指定(ClickClientScrap::FX::EXPIRATION_TYPE_SPECIFIED)」の場合の有効期限をDateで指定します。(有効期限が「日付指定」の場合、必須)
263
+ # - <b>IFD注文</b> ※決済取引の指定があればIFD取引となります。
264
+ # - <tt>:rate</tt> .. 注文レート(必須)
265
+ # - <tt>:execution_expression</tt> .. 執行条件。ClickClientScrap::FX::EXECUTION_EXPRESSION_LIMIT_ORDER等を指定します(必須)
266
+ # - <tt>:expiration_type</tt> .. 有効期限。ClickClientScrap::FX::EXPIRATION_TYPE_TODAY等を指定します(必須)
267
+ # - <tt>:expiration_date</tt> .. 有効期限が「日付指定(ClickClientScrap::FX::EXPIRATION_TYPE_SPECIFIED)」の場合の有効期限をDateで指定します。(有効期限が「日付指定」の場合、必須)
268
+ # - <tt>:settle</tt> .. 決済取引の指定。マップで指定します。
269
+ # - <tt>:unit</tt> .. 決済取引の取引数量(必須)
270
+ # - <tt>:sell_or_buy</tt> .. 決済取引の売買区分。ClickClientScrap::FX::BUY,ClickClientScrap::FX::SELLのいずれかを指定します。(必須)
271
+ # - <tt>:rate</tt> .. 決済取引の注文レート(必須)
272
+ # - <tt>:execution_expression</tt> .. 決済取引の執行条件。ClickClientScrap::FX::EXECUTION_EXPRESSION_LIMIT_ORDER等を指定します(必須)
273
+ # - <tt>:expiration_type</tt> .. 決済取引の有効期限。ClickClientScrap::FX::EXPIRATION_TYPE_TODAY等を指定します(必須)
274
+ # - <tt>:expiration_date</tt> .. 決済取引の有効期限が「日付指定(ClickClientScrap::FX::EXPIRATION_TYPE_SPECIFIED)」の場合の有効期限をDateで指定します。(有効期限が「日付指定」の場合、必須)
275
+ # - <b>IFD-OCO注文</b> ※決済取引の指定と逆指値レートの指定があればIFD-OCO取引となります。
276
+ # - <tt>:rate</tt> .. 注文レート(必須)
277
+ # - <tt>:execution_expression</tt> .. 執行条件。ClickClientScrap::FX::EXECUTION_EXPRESSION_LIMIT_ORDER等を指定します(必須)
278
+ # - <tt>:expiration_type</tt> .. 有効期限。ClickClientScrap::FX::EXPIRATION_TYPE_TODAY等を指定します(必須)
279
+ # - <tt>:expiration_date</tt> .. 有効期限が「日付指定(ClickClientScrap::FX::EXPIRATION_TYPE_SPECIFIED)」の場合の有効期限をDateで指定します。(有効期限が「日付指定」の場合、必須)
280
+ # - <tt>:settle</tt> .. 決済取引の指定。マップで指定します。
281
+ # - <tt>:unit</tt> .. 決済取引の取引数量(必須)
282
+ # - <tt>:sell_or_buy</tt> .. 決済取引の売買区分。ClickClientScrap::FX::BUY,ClickClientScrap::FX::SELLのいずれかを指定します。(必須)
283
+ # - <tt>:rate</tt> .. 決済取引の注文レート(必須)
284
+ # - <tt>:stop_order_rate</tt> .. 決済取引の逆指値レート(必須)
285
+ # - <tt>:expiration_type</tt> .. 決済取引の有効期限。ClickClientScrap::FX::EXPIRATION_TYPE_TODAY等を指定します(必須)
286
+ # - <tt>:expiration_date</tt> .. 決済取引の有効期限が「日付指定(ClickClientScrap::FX::EXPIRATION_TYPE_SPECIFIED)」の場合の有効期限をDateで指定します。(有効期限が「日付指定」の場合、必須)
287
+ #戻り値:: ClickClientScrap::FX::OrderResult
288
+ #
289
+ def order ( currency_pair_code, sell_or_buy, unit, options={} )
290
+
291
+ # 取り引き種別の判別とパラメータチェック
292
+ type = ORDER_TYPE_MARKET_ORDER
293
+ if ( options && options[:settle] != nil )
294
+ if ( options[:settle][:stop_order_rate] != nil)
295
+ # 逆指値レートと決済取引の指定があればIFD-OCO取引
296
+ raise "options[:settle][:rate] is required." if options[:settle][:rate] == nil
297
+ type = ORDER_TYPE_IFD_OCO
298
+ else
299
+ # 決済取引の指定のみがあればIFD取引
300
+ raise "options[:settle][:rate] is required." if options[:settle][:rate] == nil
301
+ raise "options[:settle][:execution_expression] is required." if options[:settle][:execution_expression] == nil
302
+ type = ORDER_TYPE_IFD
303
+ end
304
+ raise "options[:rate] is required." if options[:rate] == nil
305
+ raise "options[:execution_expression] is required." if options[:execution_expression] == nil
306
+ raise "options[:expiration_type] is required." if options[:expiration_type] == nil
307
+ raise "options[:settle][:rate] is required." if options[:settle][:rate] == nil
308
+ raise "options[:settle][:sell_or_buy] is required." if options[:settle][:sell_or_buy] == nil
309
+ raise "options[:settle][:unit] is required." if options[:settle][:unit] == nil
310
+ raise "options[:settle][:expiration_type] is required." if options[:expiration_type] == nil
311
+ elsif ( options && options[:rate] != nil )
312
+ if ( options[:stop_order_rate] != nil )
313
+ # 逆指値レートが指定されていればOCO取引
314
+ type = ORDER_TYPE_OCO
315
+ else
316
+ # そうでなければ通常取引
317
+ raise "options[:execution_expression] is required." if options[:execution_expression] == nil
318
+ type = ORDER_TYPE_NORMAL
319
+ end
320
+ raise "options[:expiration_type] is required." if options[:expiration_type] == nil
321
+ else
322
+ # 成り行き
323
+ type = ORDER_TYPE_MARKET_ORDER
324
+ end
325
+
326
+ #注文前の注文一覧
327
+ before = list_orders( ORDER_CONDITION_ON_ORDER ).inject(Set.new) {|s,o| s << o[0]; s }
328
+
329
+ # レート一覧
330
+ result = link_click( "1" )
331
+
332
+ ClickClientScrap::Client.error( result ) if result.forms.empty?
333
+ form = result.forms.first
334
+
335
+ # 通貨ペア
336
+ option = form.fields.find{|f| f.name == "P001" }.options.find {|o|
337
+ to_pair( o.text.strip ) == currency_pair_code
338
+ }
339
+ raise "illegal currency_pair_code. currency_pair_code=#{currency_pair_code.to_s}" unless option
340
+ option.select
341
+
342
+ #注文方式
343
+ form["P100"] = type
344
+
345
+ # 詳細設定画面へ
346
+ result = @client.submit(form)
347
+ ClickClientScrap::Client.error( result ) if result.forms.empty?
348
+ form = result.forms.first
349
+ case type
350
+ when ORDER_TYPE_MARKET_ORDER
351
+ # 成り行き
352
+ form["P002"] = unit.to_s # 取り引き数量
353
+ form["P005.0"] = sell_or_buy == ClickClientScrap::FX::SELL ? "1" : "0" #売り/買い
354
+ form["P007"] = options[:slippage].to_s if ( options && options[:slippage] != nil ) # スリッページ
355
+ when ORDER_TYPE_NORMAL
356
+ # 指値
357
+ form["P003"] = options[:rate].to_s # レート
358
+ form["P005"] = unit.to_s # 取り引き数量
359
+ form["P002.0"] = sell_or_buy == ClickClientScrap::FX::SELL ? "1" : "0" #売り/買い
360
+ exp = options[:execution_expression]
361
+ form["P004.0"] = exp == ClickClientScrap::FX::EXECUTION_EXPRESSION_REVERSE_LIMIT_ORDER ? "2" : "1" #指値/逆指値
362
+ set_expiration( form, options, "P008", "P009" ) # 有効期限
363
+ when ORDER_TYPE_OCO
364
+ # OCO
365
+ form["P003"] = options[:rate].to_s # レート
366
+ form["P005"] = options[:stop_order_rate].to_s # 逆指値レート
367
+ form["P007"] = unit.to_s # 取り引き数量
368
+ form["P002.0"] = sell_or_buy == ClickClientScrap::FX::SELL ? "1" : "0" #売り/買い
369
+ set_expiration( form, options, "P010", "P011" ) # 有効期限
370
+ else
371
+ raise "not supported yet."
372
+ end
373
+
374
+ # 確認画面へ
375
+ result = @client.submit(form)
376
+ ClickClientScrap::Client.error( result ) if result.forms.empty?
377
+ result = @client.submit(result.forms.first)
378
+ ClickClientScrap::Client.error( result ) unless result.body.toutf8 =~ /注文受付完了/
379
+
380
+ #注文前の一覧と注文後の一覧を比較して注文を割り出す。
381
+ #成り行き注文の場合、即座に約定するのでnilになる(タイミングによっては取得できるかも)
382
+ tmp = list_orders( ORDER_CONDITION_ON_ORDER ).find {|o| !before.include?(o[0]) }
383
+ return OrderResult.new( tmp ? tmp[1].order_no : nil )
384
+ end
385
+
386
+ # 有効期限を設定する
387
+ #form:: フォーム
388
+ #options:: パラメータ
389
+ #input_type:: 有効期限の種別を入力するinput要素名
390
+ #input_date:: 有効期限が日付指定の場合に、日付を入力するinput要素名
391
+ def set_expiration( form, options, input_type, input_date )
392
+ case options[:expiration_type]
393
+ when ClickClientScrap::FX::EXPIRATION_TYPE_TODAY
394
+ form[input_type] = "0"
395
+ when ClickClientScrap::FX::EXPIRATION_TYPE_WEEK_END
396
+ form[input_type] = "1"
397
+ when ClickClientScrap::FX::EXPIRATION_TYPE_SPECIFIED
398
+ form[input_type] = "3"
399
+ raise "options[:expiration_date] is required." unless options[:expiration_date]
400
+ form["#{input_date}.Y"] = options[:expiration_date].year
401
+ form["#{input_date}.M"] = options[:expiration_date].month
402
+ form["#{input_date}.D"] = options[:expiration_date].day
403
+ form["#{input_date}.h"] = options[:expiration_date].respond_to?(:hour) ? options[:expiration_date].hour : "0"
404
+ else
405
+ form[input_type] = "2"
406
+ end
407
+ end
408
+
409
+ #
410
+ #=== 注文をキャンセルします。
411
+ #
412
+ #order_no:: 注文番号
413
+ #戻り値:: なし
414
+ #
415
+ def cancel_order( order_no )
416
+
417
+ raise "order_no is nil." unless order_no
418
+
419
+ # 注文一覧
420
+ result = link_click( "2" )
421
+ ClickClientScrap::Client.error( result ) if result.forms.empty?
422
+ form = result.forms.first
423
+ form["P002"] = ORDER_CONDITION_ON_ORDER
424
+ result = @client.submit(form)
425
+
426
+ # 対象となる注文をクリック
427
+ link = result.links.find {|l|
428
+ l.href =~ /[^"]*GKEY=([a-zA-Z0-9]*)[^"]*/ && $1 == order_no
429
+ }
430
+ raise "illegal order_no. order_no=#{order_no}" unless link
431
+ result = @client.click(link)
432
+ ClickClientScrap::Client.error( result ) if result.forms.empty?
433
+
434
+ # キャンセル
435
+ form = result.forms[1]
436
+ result = @client.submit(form)
437
+ ClickClientScrap::Client.error( result ) if result.forms.empty?
438
+ form = result.forms.first
439
+ result = @client.submit(form)
440
+ ClickClientScrap::Client.error( result ) unless result.body.toutf8 =~ /注文取消受付完了/
441
+ end
442
+
443
+
444
+ #
445
+ #=== 決済注文を行います。
446
+ #
447
+ #*open_interest_id*:: 決済する建玉番号
448
+ #*unit*:: 取引数量
449
+ #*options*:: 決済注文のオプション。注文方法に応じて以下の情報を設定できます。
450
+ # - <b>成り行き注文</b>
451
+ # - <tt>:slippage</tt> .. スリッページ (オプション)
452
+ # - <tt>:slippage_base_rate</tt> .. スリッページの基準となる取引レート(スリッページが指定された場合、必須。)
453
+ # - <b>通常注文</b> <b>※未実装</b> ※注文レートが設定されていれば通常取引となります。
454
+ # - <tt>:rate</tt> .. 注文レート(必須)
455
+ # - <tt>:execution_expression</tt> .. 執行条件。ClickClientScrap::FX::EXECUTION_EXPRESSION_LIMIT_ORDER等を指定します(必須)
456
+ # - <tt>:expiration_type</tt> .. 有効期限。ClickClientScrap::FX::EXPIRATION_TYPE_TODAY等を指定します(必須)
457
+ # - <tt>:expiration_date</tt> .. 有効期限が「日付指定(ClickClientScrap::FX::EXPIRATION_TYPE_SPECIFIED)」の場合の有効期限をDateで指定します。(有効期限が「日付指定」の場合、必須)
458
+ # - <b>OCO注文</b> <b>※未実装</b> ※注文レートと逆指値レートが設定されていればOCO取引となります。
459
+ # - <tt>:rate</tt> .. 注文レート(必須)
460
+ # - <tt>:stop_order_rate</tt> .. 逆指値レート(必須)
461
+ # - <tt>:expiration_type</tt> .. 有効期限。ClickClientScrap::FX::EXPIRATION_TYPE_TODAY等を指定します(必須)
462
+ # - <tt>:expiration_date</tt> .. 有効期限が「日付指定(ClickClientScrap::FX::EXPIRATION_TYPE_SPECIFIED)」の場合の有効期限をDateで指定します。(有効期限が「日付指定」の場合、必須)
463
+ #<b>戻り値</b>:: なし
464
+ #
465
+ def settle ( open_interest_id, unit, options={} )
466
+ if ( options[:rate] != nil && options[:stop_order_rate] != nil )
467
+ # レートと逆指値レートが指定されていればOCO取引
468
+ raise "options[:expiration_type] is required." if options[:expiration_type] == nil
469
+ elsif ( options[:rate] != nil )
470
+ # レートが指定されていれば通常取引
471
+ raise "options[:execution_expression] is required." if options[:execution_expression] == nil
472
+ raise "options[:expiration_type] is required." if options[:expiration_type] == nil
473
+ else
474
+ # 成り行き
475
+ if ( options[:slippage] != nil )
476
+ raise "if you use a slippage, options[:slippage_base_rate] is required." if options[:slippage_base_rate] == nil
477
+ end
478
+ end
479
+
480
+ # 建玉一覧
481
+ result = link_click( "3" )
482
+
483
+ # 対象となる建玉をクリック
484
+ link = result.links.find {|l|
485
+ l.href =~ /[^"]*ORDERNO=([a-zA-Z0-9]*)[^"]*/ && $1 == open_interest_id
486
+ }
487
+ raise "illegal open_interest_id. open_interest_id=#{open_interest_id}" unless link
488
+ result = @client.click(link)
489
+
490
+ # 決済
491
+ form = result.forms.first
492
+ form["P100"] = "00" # 成り行き TODO 通常(01),OCO取引(21)対応
493
+ result = @client.submit(form)
494
+ ClickClientScrap::Client.error( result ) if result.forms.empty?
495
+
496
+ # 設定
497
+ form = result.forms.first
498
+ form["L111"] = unit.to_s
499
+ form["P005"] = options[:slippage].to_s if options[:slippage]
500
+ result = @client.submit(form)
501
+ ClickClientScrap::Client.error( result ) if result.forms.empty?
502
+
503
+ # 確認
504
+ form = result.forms.first
505
+ result = @client.submit(form)
506
+ ClickClientScrap::Client.error( result ) unless result.body.toutf8 =~ /完了/
507
+ end
508
+
509
+
510
+ #
511
+ #=== 注文一覧を取得します。
512
+ #
513
+ #order_condition_code:: 注文状況コード(必須)
514
+ #currency_pair_code:: 通貨ペアコード <b>※未実装</b>
515
+ #戻り値:: 注文番号をキーとするClickClientScrap::FX::Orderのハッシュ。
516
+ #
517
+ def list_orders( order_condition_code=ClickClientScrap::FX::ORDER_CONDITION_ALL, currency_pair_code=nil )
518
+ result = link_click( "2" )
519
+ ClickClientScrap::Client.error( result ) if result.forms.empty?
520
+ form = result.forms.first
521
+ form["P001"] = "" # TODO currency_pair_codeでの絞り込み
522
+ form["P002"] = order_condition_code
523
+ result = @client.submit(form)
524
+
525
+ list = result.body.toutf8.scan( /<a href="[^"]*GKEY=([a-zA-Z0-9]*)">([A-Z]{3}\/[A-Z]{3}) ([^<]*)<\/a><br>[^;]*;([^<]*)<font[^>]*>([^<]*)<\/font>([^@]*)@([\d\.]*)([^\s]*) ([^<]*)<br>/m )
526
+ tmp = {}
527
+ list.each {|i|
528
+ order_no = i[0]
529
+ order_type = to_order_type_code(i[2])
530
+ trade_type = i[3] == "新" ? ClickClientScrap::FX::TRADE_TYPE_NEW : ClickClientScrap::FX::TRADE_TYPE_SETTLEMENT
531
+ pair = to_pair( i[1] )
532
+ sell_or_buy = i[4] == "売" ? ClickClientScrap::FX::SELL : ClickClientScrap::FX::BUY
533
+ count = pair == :ZARJPY ? i[5].to_i/10 : i[5].to_i
534
+ rate = i[6].to_f
535
+ execution_expression = if i[7] == "指"
536
+ ClickClientScrap::FX::EXECUTION_EXPRESSION_LIMIT_ORDER
537
+ elsif i[7] == "逆"
538
+ ClickClientScrap::FX::EXECUTION_EXPRESSION_REVERSE_LIMIT_ORDER
539
+ else
540
+ ClickClientScrap::FX::EXECUTION_EXPRESSION_MARKET_ORDER
541
+ end
542
+ tmp[order_no] = Order.new( order_no, trade_type, order_type, execution_expression, sell_or_buy, pair, count, rate, i[8])
543
+ }
544
+ return tmp
545
+ end
546
+
547
+ #
548
+ #=== 建玉一覧を取得します。
549
+ #
550
+ #currency_pair_code:: 通貨ペアコード。<b>※未実装</b>
551
+ #戻り値:: 建玉IDをキーとするClickClientScrap::FX::OpenInterestのハッシュ。
552
+ #
553
+ def list_open_interests( currency_pair_code=nil )
554
+ result = link_click( "3" )
555
+ ClickClientScrap::Client.error( result ) if result.forms.empty?
556
+ form = result.forms.first
557
+ form["P001"] = "" # TODO currency_pair_codeでの絞り込み
558
+ result = @client.submit(form)
559
+
560
+ list = result.body.toutf8.scan( /<a href="[^"]*">([A-Z]{3}\/[A-Z]{3}):([^<]*)<\/a><br>[^;]*;<font[^>]*>([^<]*)<\/font>([\d\.]*)[^\s@]*@([\d\.]*).*?<font[^>]*>([^<]*)<\/font>/m )
561
+ tmp = {}
562
+ list.each {|i|
563
+ open_interest_id = i[1]
564
+ pair = to_pair( i[0] )
565
+ sell_or_buy = i[2] == "売" ? ClickClientScrap::FX::SELL : ClickClientScrap::FX::BUY
566
+ count = i[3].to_i
567
+ rate = i[4].to_f
568
+ profit_or_loss = i[5].to_i
569
+ tmp[open_interest_id] = OpenInterest.new(open_interest_id, pair, sell_or_buy, count, rate, profit_or_loss )
570
+ }
571
+ return tmp
572
+ end
573
+
574
+ #
575
+ #=== 余力情報を取得します。
576
+ #
577
+ #戻り値:: ClickClientScrap::FX::Marginのハッシュ。
578
+ #
579
+ def get_margin
580
+ result = link_click( "7" )
581
+ list = result.body.toutf8.scan( /【([^<]*)[^>]*>[^>]*>([^<]*)</m )
582
+ values = list.inject({}) {|r,i|
583
+ if ( i[0] == "証拠金維持率】" )
584
+ r[i[0]] = i[1]
585
+ else
586
+ r[i[0]] = i[1].gsub(/,/, "").to_i
587
+ end
588
+ r
589
+ }
590
+ return Margin.new(
591
+ values["時価評価総額】"],
592
+ values["建玉評価損益】"],
593
+ values["口座残高】"],
594
+ values["証拠金維持率】"],
595
+ values["余力】"],
596
+ values["拘束証拠金】"],
597
+ values["必要証拠金】"],
598
+ values["注文中必要証拠金】"],
599
+ values["振替可能額】"]
600
+ )
601
+ end
602
+
603
+ # ログアウトします。
604
+ def logout
605
+ @client.click( @links.find {|i|
606
+ i.text == "\303\233\302\270\303\236\302\261\302\263\303\204"
607
+ })
608
+ end
609
+
610
+ private
611
+ # "USD/JPY"を:USDJPYのようなシンボルに変換します。
612
+ def to_pair( str )
613
+ str.gsub( /\//, "" ).to_sym
614
+ end
615
+
616
+ # 注文種別を注文種別コードに変換します。
617
+ def to_order_type_code( order_type )
618
+ return case order_type
619
+ when "成行注文"
620
+ ClickClientScrap::FX::ORDER_TYPE_MARKET_ORDER
621
+ when "通常注文"
622
+ ClickClientScrap::FX::ORDER_TYPE_NORMAL
623
+ when "OCO注文"
624
+ ClickClientScrap::FX::ORDER_TYPE_OCO
625
+ when "IFD注文"
626
+ ClickClientScrap::FX::ORDER_TYPE_IFD
627
+ when "IFD-OCO注文"
628
+ ClickClientScrap::FX::ORDER_TYPE_IFD_OCO
629
+ else
630
+ raise "illegal order_type. order_type=#{order_type}"
631
+ end
632
+ end
633
+
634
+ def link_click( no )
635
+ link = @links.find {|i|
636
+ i.attributes["accesskey"] == no
637
+ }
638
+ raise "link isnot found. accesskey=#{no}" unless link
639
+ @client.click( link )
640
+ end
641
+ end
642
+
643
+ # オプション
644
+ attr :options, true
645
+
646
+ #=== スワップ
647
+ Swap = Struct.new(:pair, :sell_swap, :buy_swap)
648
+ #=== レート
649
+ Rate = Struct.new(:pair, :bid_rate, :ask_rate, :sell_swap, :buy_swap )
650
+ #===注文
651
+ Order = Struct.new(:order_no, :trade_type, :order_type, :execution_expression, :sell_or_buy, :pair, :count, :rate, :order_state )
652
+ #===注文結果
653
+ OrderResult = Struct.new(:order_no )
654
+ #===建玉
655
+ OpenInterest = Struct.new(:open_interest_id, :pair, :sell_or_buy, :count, :rate, :profit_or_loss )
656
+ #===余力
657
+ Margin = Struct.new(
658
+ :market_value, #時価評価の総額
659
+ :appraisal_profit_or_loss_of_open_interest, #建玉の評価損益
660
+ :balance_in_account, # 口座残高
661
+ :guarantee_money_maintenance_ratio, #証拠金の維持率
662
+ :margin, #余力
663
+ :freezed_guarantee_money, #拘束されている証拠金
664
+ :required_guarantee_money, #必要な証拠金
665
+ :ordered_guarantee_money, #注文中の証拠金
666
+ :transferable_money_amount #振替可能額
667
+ )
668
+ end
669
+ end
670
+
671
+
672
+
@@ -0,0 +1,133 @@
1
+
2
+ require 'rubygems'
3
+ require 'jiji/plugin/securities_plugin'
4
+ require 'clickclient_scrap'
5
+ require 'thread'
6
+
7
+ # クリック証券アクセスプラグイン
8
+ class ClickSecuritiesPlugin
9
+ include JIJI::Plugin::SecuritiesPlugin
10
+
11
+ #プラグインの識別子を返します。
12
+ def plugin_id
13
+ :click_securities
14
+ end
15
+ #プラグインの表示名を返します。
16
+ def display_name
17
+ "CLICK Securities"
18
+ end
19
+ #「jiji setting」でユーザーに入力を要求するデータの情報を返します。
20
+ def input_infos
21
+ [ Input.new( :user, "Please input a user name of CLICK Securities.", false, nil ),
22
+ Input.new( :password, "Please input a password of CLICK Securities.", true, nil ),
23
+ Input.new( :proxy, "Please input a proxy. example: http://example.com:80 (default: nil )", false, nil ) ]
24
+ end
25
+
26
+ #プラグインを初期化します。
27
+ def init_plugin( props, logger )
28
+ @session = ClickSecuritiesPluginSession.new( props, logger )
29
+ end
30
+ #プラグインを破棄します。
31
+ def destroy_plugin
32
+ @session.close
33
+ end
34
+
35
+ #利用可能な通貨ペア一覧を取得します。
36
+ def list_pairs
37
+ return ALL_PAIRS.map {|pair|
38
+ Pair.new( pair, pair == ClickClientScrap::FX::ZARJPY ? 100000 : 10000 )
39
+ }
40
+ end
41
+
42
+ #現在のレートを取得します。
43
+ def list_rates
44
+ @session.list_rates.inject({}) {|r,p|
45
+ r[p[0]] = Rate.new( p[1].bid_rate, p[1].ask_rate, p[1].sell_swap, p[1].buy_swap )
46
+ r
47
+ }
48
+ end
49
+
50
+ #成り行きで発注を行います。
51
+ def order( pair, sell_or_buy, count )
52
+
53
+ # 建玉一覧を取得
54
+ before = @session.list_open_interests.inject( Set.new ) {|s,i| s << i[0]; s }
55
+ # 発注
56
+ @session.order( pair, sell_or_buy == :buy ? ClickClientScrap::FX::BUY : ClickClientScrap::FX::SELL, count )
57
+ # 建玉を特定
58
+ position = nil
59
+ # 10s待っても取得できなければあきらめる
60
+ 20.times {|i|
61
+ sleep 0.5
62
+ position = @session.list_open_interests.find {|i| !before.include?(i[0]) }
63
+ break if position
64
+ }
65
+ raise "order fialed." unless position
66
+ return JIJI::Plugin::SecuritiesPlugin::Position.new( position[1].open_interest_id )
67
+ end
68
+
69
+ #建玉を決済します。
70
+ def commit( position_id, count )
71
+ @session.settle( position_id, count )
72
+ end
73
+
74
+ private
75
+
76
+ ALL_PAIRS = [
77
+ ClickClientScrap::FX::USDJPY, ClickClientScrap::FX::EURJPY,
78
+ ClickClientScrap::FX::GBPJPY, ClickClientScrap::FX::AUDJPY,
79
+ ClickClientScrap::FX::NZDJPY, ClickClientScrap::FX::CADJPY,
80
+ ClickClientScrap::FX::CHFJPY, ClickClientScrap::FX::ZARJPY,
81
+ ClickClientScrap::FX::EURUSD, ClickClientScrap::FX::GBPUSD,
82
+ ClickClientScrap::FX::AUDUSD, ClickClientScrap::FX::EURCHF,
83
+ ClickClientScrap::FX::GBPCHF, ClickClientScrap::FX::USDCHF
84
+ ]
85
+ end
86
+
87
+ class ClickSecuritiesPluginSession
88
+ def initialize( props, logger )
89
+ @props = props
90
+ @logger = logger
91
+ @m = Mutex.new
92
+ end
93
+ def method_missing( name, *args )
94
+ @m.synchronize {
95
+ begin
96
+ session.send( name, *args )
97
+ rescue
98
+ # エラーになった場合はセッションを再作成する
99
+ close
100
+ raise $!
101
+ end
102
+ }
103
+ end
104
+ def close
105
+ begin
106
+ @session.logout if @session
107
+ rescue
108
+ @logger.error $!
109
+ ensure
110
+ @session = nil
111
+ @client = nil
112
+ end
113
+ end
114
+ def session
115
+ begin
116
+ proxy = nil
117
+ if @props.key?(:proxy) && @props[:proxy] != nil && @props[:proxy].length > 0
118
+ proxy = @props[:proxy]
119
+ end
120
+ @client ||= ClickClientScrap::Client.new( proxy )
121
+ @session ||= @client.fx_session( @props[:user], @props[:password] )
122
+ rescue
123
+ @logger.error $!
124
+ raise $!
125
+ end
126
+ @session
127
+ end
128
+ end
129
+
130
+ JIJI::Plugin.register(
131
+ JIJI::Plugin::SecuritiesPlugin::FUTURE_NAME,
132
+ ClickSecuritiesPlugin.new )
133
+
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/ruby
2
+
3
+ $: << "../lib"
4
+
5
+ require "runit/testcase"
6
+ require "runit/cui/testrunner"
7
+ require 'clickclient_scrap'
8
+
9
+ class FxSessionTest < RUNIT::TestCase
10
+
11
+ #convert_rate のテスト
12
+ def test_convert_rate
13
+ rate = ClickClient::FX::FxSession.convert_rate("123.34-37").map {|i| i.to_s }
14
+ assert_equals rate, [ "123.34", "123.37" ]
15
+
16
+ rate = ClickClient::FX::FxSession.convert_rate("123.534-37").map {|i| i.to_s }
17
+ assert_equals rate, [ "123.534", "123.537" ]
18
+
19
+ rate = ClickClient::FX::FxSession.convert_rate("123.594-02").map {|i| i.to_s }
20
+ assert_equals rate, [ "123.594", "123.602" ]
21
+
22
+ rate = ClickClient::FX::FxSession.convert_rate("123.00-02").map {|i| i.to_s }
23
+ assert_equals rate, [ "123.0", "123.02" ]
24
+
25
+ rate = ClickClient::FX::FxSession.convert_rate("123.34-33").map {|i| i.to_s }
26
+ assert_equals rate, [ "123.34", "124.33" ]
27
+
28
+ rate = ClickClient::FX::FxSession.convert_rate("123.34-34").map {|i| i.to_s }
29
+ assert_equals rate, [ "123.34", "123.34" ]
30
+
31
+ rate = ClickClient::FX::FxSession.convert_rate("0.334-335").map {|i| i.to_s }
32
+ assert_equals rate, [ "0.334", "0.335" ]
33
+
34
+ rate = ClickClient::FX::FxSession.convert_rate("0.334-333").map {|i| i.to_s }
35
+ assert_equals rate, [ "0.334", "1.333" ]
36
+ end
37
+
38
+ end
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/ruby
2
+
3
+ $: << "../lib"
4
+
5
+ require "runit/testcase"
6
+ require "logger"
7
+ require "runit/cui/testrunner"
8
+ require 'clickclient_scrap'
9
+ require 'jiji/plugin/plugin_loader'
10
+ require 'jiji/plugin/securities_plugin'
11
+
12
+ # jijiプラグインのテスト
13
+ # ※実際に取引を行うので注意!
14
+ class JIJIPluginTest < RUNIT::TestCase
15
+
16
+ def setup
17
+ @logger = Logger.new STDOUT
18
+ @user = IO.read( "../sample/user" )
19
+ @pass = IO.read( "../sample/pass" )
20
+ end
21
+
22
+ def test_basic
23
+ # ロード
24
+ JIJI::Plugin::Loader.new.load
25
+ plugins = JIJI::Plugin.get( JIJI::Plugin::SecuritiesPlugin::FUTURE_NAME )
26
+ plugin = plugins.find {|i| i.plugin_id == :click_securities }
27
+ assert_not_nil plugin
28
+ assert_equals plugin.display_name, "CLICK Securities"
29
+
30
+ begin
31
+ plugin.init_plugin( {:user=>@user, :password=>@pass}, @logger )
32
+
33
+ # 利用可能な通貨ペア一覧とレート
34
+ pairs = plugin.list_pairs
35
+ rates = plugin.list_rates
36
+ pairs.each {|p|
37
+ # 利用可能とされたペアのレートが取得できていることを確認
38
+ assert_not_nil p.name
39
+ assert_not_nil p.trade_unit
40
+ assert_not_nil rates[p.name]
41
+ assert_not_nil rates[p.name].bid
42
+ assert_not_nil rates[p.name].ask
43
+ assert_not_nil rates[p.name].sell_swap
44
+ assert_not_nil rates[p.name].buy_swap
45
+ }
46
+ sleep 1
47
+
48
+ 3.times {
49
+ rates = plugin.list_rates
50
+ pairs.each {|p|
51
+ # 利用可能とされたペアのレートが取得できていることを確認
52
+ assert_not_nil p.name
53
+ assert_not_nil p.trade_unit
54
+ assert_not_nil rates[p.name]
55
+ assert_not_nil rates[p.name].bid
56
+ assert_not_nil rates[p.name].ask
57
+ assert_not_nil rates[p.name].sell_swap
58
+ assert_not_nil rates[p.name].buy_swap
59
+ }
60
+ sleep 10
61
+ }
62
+
63
+ # # 売り/買い
64
+ # sell = plugin.order( :USDJPY, :sell, 1 )
65
+ # buy = plugin.order( :USDJPY, :buy, 1 )
66
+ # assert_not_nil sell.position_id
67
+ # assert_not_nil buy.position_id
68
+ #
69
+ # # 約定
70
+ # plugin.commit sell.position_id, 1
71
+ # plugin.commit buy.position_id, 1
72
+ ensure
73
+ plugin.destroy_plugin
74
+ end
75
+ end
76
+
77
+ end
metadata ADDED
@@ -0,0 +1,71 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: clickclient_scrap
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.7
5
+ platform: ruby
6
+ authors:
7
+ - Masaya Yamauchi
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-12-12 00:00:00 +09:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: mechanize
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 0.9.3
24
+ version:
25
+ description:
26
+ email: y-masaya@red.hot.co.jp
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files:
32
+ - README
33
+ files:
34
+ - README
35
+ - ChangeLog
36
+ - lib/clickclient_scrap.rb
37
+ - lib/jiji_plugin.rb
38
+ - test/test_FxSession.rb
39
+ - test/test_jiji_plugin.rb
40
+ has_rdoc: true
41
+ homepage: http://github.com/unageanu/clickclient_scrap/tree/master
42
+ licenses: []
43
+
44
+ post_install_message:
45
+ rdoc_options:
46
+ - --main
47
+ - README
48
+ require_paths:
49
+ - lib
50
+ required_ruby_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: "0"
55
+ version:
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: "0"
61
+ version:
62
+ requirements: []
63
+
64
+ rubyforge_project:
65
+ rubygems_version: 1.3.5
66
+ signing_key:
67
+ specification_version: 3
68
+ summary: click securities client library for ruby.
69
+ test_files:
70
+ - test/test_FxSession.rb
71
+ - test/test_jiji_plugin.rb