clickclient_scrap 0.1.7

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.
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