sbiclient 0.1.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.
data/lib/sbiclient.rb ADDED
@@ -0,0 +1,724 @@
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
+ #=== SBI証券アクセスクライアント
12
+ #
13
+ #*License*:: Ruby ライセンスに準拠
14
+ #
15
+ #SBI証券を利用するためのクライアントライブラリです。携帯向けサイトのスクレイピングにより以下の機能を提供します。
16
+ #- 外為証拠金取引(FX)取引
17
+ #
18
+ #====基本的な使い方
19
+ #
20
+ # require 'sbiclient'
21
+ #
22
+ # c = SBIClient::Client.new
23
+ # # c = SBIClient::Client.new https://<プロキシホスト>:<プロキシポート> # プロキシを利用する場合
24
+ # c.fx_session( "<ユーザー名>", "<パスワード>" ) { | fx_session |
25
+ # # レート情報一覧取得
26
+ # list = fx_session.list_rates
27
+ # puts list
28
+ # }
29
+ #
30
+ #====免責
31
+ #- 本ライブラリの利用は自己責任でお願いします。
32
+ #- ライブラリの不備・不具合等によるあらゆる損害について、作成者は責任を負いません。
33
+ #
34
+ module SBIClient
35
+
36
+ # クライアント
37
+ class Client
38
+ # ホスト名
39
+ DEFAULT_HOST_NAME = "https://mobile.sbisec.co.jp/web/visitor/loginUser.do"
40
+
41
+ #
42
+ #===コンストラクタ
43
+ #
44
+ #*proxy*:: プロキシホストを利用する場合、そのホスト名とパスを指定します。
45
+ # 例) https://proxyhost.com:80
46
+ #
47
+ def initialize( proxy=nil )
48
+ @client = WWW::Mechanize.new {|c|
49
+ # プロキシ
50
+ if proxy
51
+ uri = URI.parse( proxy )
52
+ c.set_proxy( uri.host, uri.port )
53
+ end
54
+ }
55
+ @client.keep_alive = false
56
+ @client.max_history=0
57
+ WWW::Mechanize::AGENT_ALIASES["KDDI-CA39"] = \
58
+ 'KDDI-CA39 UP.Browser/6.2.0.13.1.5 (GUI) MMP/2.0'
59
+ @client.user_agent_alias = "KDDI-CA39"
60
+ @host_name = DEFAULT_HOST_NAME
61
+ end
62
+
63
+ #ログインし、セッションを開始します。
64
+ #-ブロックを指定した場合、引数としてセッションを指定してブロックを実行します。ブロック実行後、ログアウトします。
65
+ #-そうでない場合、セッションを返却します。この場合、SBIClient::FX::FxSession#logoutを実行しログアウトしてください。
66
+ #
67
+ #userid:: ユーザーID
68
+ #password:: パスワード
69
+ #order_password:: 取引パスワード
70
+ #options:: オプション
71
+ #戻り値:: SBIClient::FX::FxSession
72
+ def fx_session( userid, password, order_password, options={}, &block )
73
+ # ログイン
74
+ page = @client.get(@host_name)
75
+ SBIClient::Client.error(page) if page.forms.length <= 0
76
+ form = page.forms.first
77
+ form.username = userid
78
+ form.password = password
79
+ result = @client.submit(form, form.buttons.first)
80
+ SBIClient::Client.error(result) unless result.title.toutf8 =~ /SBI証券.*メンバートップ/
81
+ session = FX::FxSession.new( @client, order_password, result, options )
82
+ if block_given?
83
+ begin
84
+ yield session
85
+ ensure
86
+ session.logout
87
+ end
88
+ else
89
+ return session
90
+ end
91
+ end
92
+ def self.error( page )
93
+ msgs = page.body.scan( /<[fF][oO][nN][tT]\s+[cC][oO][lL][oO][rR]="?#FF0000"?>([^<]*)</ ).flatten
94
+ error = !msgs.empty? ? msgs.map{|m| m.strip}.join(",") : page.body
95
+ raise "operation failed.detail=#{error}".toutf8
96
+ end
97
+
98
+ #ホスト名
99
+ attr :host_name, true
100
+ end
101
+
102
+ module FX
103
+
104
+ # 通貨ペア: 米ドル-円
105
+ USDJPY = :USDJPY
106
+ # 通貨ペア: ユーロ-円
107
+ EURJPY = :EURJPY
108
+ # 通貨ペア: イギリスポンド-円
109
+ GBPJPY = :GBPJPY
110
+ # 通貨ペア: 豪ドル-円
111
+ AUDJPY = :AUDJPY
112
+ # 通貨ペア: ニュージーランドドル-円
113
+ NZDJPY = :NZDJPY
114
+ # 通貨ペア: カナダドル-円
115
+ CADJPY = :CADJPY
116
+ # 通貨ペア: スイスフラン-円
117
+ CHFJPY = :CHFJPY
118
+ # 通貨ペア: 南アランド-円
119
+ ZARJPY = :ZARJPY
120
+ # 通貨ペア: ユーロ-米ドル
121
+ EURUSD = :EURUSD
122
+ # 通貨ペア: イギリスポンド-米ドル
123
+ GBPUSD = :GBPUSD
124
+ # 通貨ペア: 豪ドル-米ドル
125
+ AUDUSD = :AUDUSD
126
+ # 通貨ペア: NWクローネ-円
127
+ NOKJPY = :NOKJPY
128
+ # 通貨ペア: ミニ豪ドル-円
129
+ MUDJPY = :MUDJPY
130
+ # 通貨ペア: 香港ドル-円
131
+ HKDJPY = :HKDJPY
132
+ # 通貨ペア: SWクローナ-円
133
+ SEKJPY = :SEKJPY
134
+ # 通貨ペア: ミニNZドル-円
135
+ MZDJPY = :MZDJPY
136
+ # 通貨ペア: ウォン-円
137
+ KRWJPY = :KRWJPY
138
+ # 通貨ペア: PLズロチ-円
139
+ PLNJPY = :PLNJPY
140
+ # 通貨ペア: ミニ南アランド-円
141
+ MARJPY = :MARJPY
142
+ # 通貨ペア: SGPドル-円
143
+ SGDJPY = :SGDJPY
144
+ # 通貨ペア: ミニ米ドル-円
145
+ MSDJPY = :MSDJPY
146
+ # 通貨ペア: メキシコペソ-円
147
+ MXNJPY = :MXNJPY
148
+ # 通貨ペア: ミニユーロ-円
149
+ MURJPY = :MURJPY
150
+ # 通貨ペア: トルコリラ-円
151
+ TRYJPY = :TRYJPY
152
+ # 通貨ペア: ミニポンド-円
153
+ MBPJPY = :MBPJPY
154
+ # 通貨ペア: 人民元-円
155
+ CNYJPY = :CNYJPY
156
+
157
+ # 売買区分: 買い
158
+ BUY = :BUY
159
+ # 売買区分: 売り
160
+ SELL = :SELL
161
+
162
+ # 注文タイプ: 成行
163
+ ORDER_TYPE_MARKET_ORDER = :MARKET_ORDER
164
+ # 注文タイプ: 通常
165
+ ORDER_TYPE_NORMAL = :NORMAL
166
+ # 注文タイプ: IFD
167
+ ORDER_TYPE_IFD = :IFD
168
+ # 注文タイプ: OCO
169
+ ORDER_TYPE_OCO = :OCO
170
+ # 注文タイプ: IFD-OCO
171
+ ORDER_TYPE_IFD_OCO = :IFD_OCO
172
+ # 注文タイプ: とレール
173
+ ORDER_TYPE_TRAIL = :TRAIL
174
+
175
+ # 有効期限: 当日限り
176
+ EXPIRATION_TYPE_TODAY = :EXPIRATION_TYPE_TODAY
177
+ # 有効期限: 週末まで
178
+ EXPIRATION_TYPE_WEEK_END = :EXPIRATION_TYPE_WEEK_END
179
+ # 有効期限: 日付指定
180
+ EXPIRATION_TYPE_SPECIFIED = :EXPIRATION_TYPE_SPECIFIED
181
+
182
+ # 執行条件: 成行
183
+ EXECUTION_EXPRESSION_MARKET_ORDER = :MARKET_ORDER
184
+ # 執行条件: 指値
185
+ EXECUTION_EXPRESSION_LIMIT_ORDER = :LIMIT_ORDER
186
+ # 執行条件: 逆指値
187
+ EXECUTION_EXPRESSION_REVERSE_LIMIT_ORDER = :REVERSE_LIMIT_ORDER
188
+
189
+ # トレード種別: 新規
190
+ TRADE_TYPE_NEW = :TRADE_TYPE_NEW
191
+ # トレード種別: 決済
192
+ TRADE_TYPE_SETTLEMENT = :TRADE_TYPE_SETTLEMENT
193
+
194
+ #=== FX取引のためのセッションクラス
195
+ #Client#fx_sessionのブロックの引数として渡されます。詳細はClient#fx_sessionを参照ください。
196
+ class FxSession
197
+
198
+ def initialize( client, order_password, top_page, options={} )
199
+ @client = client
200
+ @order_password = order_password
201
+ @options = options
202
+
203
+ # FXのトップ画面へ
204
+ form = top_page.forms.first
205
+ form.product_group = "sbi_fx_alpha"
206
+ result = @client.submit(form, form.buttons.first)
207
+ SBIClient::Client.error(result) unless result.content.toutf8 =~ /SBI FX α/
208
+ @links = result.links
209
+ end
210
+
211
+ #====レート一覧を取得します。
212
+ #
213
+ #戻り値:: 通貨ペアをキーとするSBIClient::FX::Rateのハッシュ。
214
+ def list_rates
215
+ #スワップの定期取得
216
+ if !@last_update_time_of_swaps \
217
+ || Time.now.to_i - @last_update_time_of_swaps > (@options[:swap_update_interval] || 60*60)
218
+ @swaps = list_swaps
219
+ @last_update_time_of_swaps = Time.now.to_i
220
+ end
221
+ rates = {}
222
+ each_rate_page {|page|
223
+ collect_rate( page, rates )
224
+ }
225
+ return rates
226
+ end
227
+
228
+ #====スワップの一覧を取得します。
229
+ #
230
+ #戻り値:: 通貨ペアをキーとするSBIClient::FX::Swapのハッシュ。
231
+ def list_swaps
232
+ swap = {}
233
+ each_rate_page {|page|
234
+ collect_swap( page, swap )
235
+ }
236
+ return swap
237
+ end
238
+
239
+ #=== 注文一覧を取得します。
240
+ #
241
+ #戻り値:: 注文番号をキーとするClickClientScrap::FX::Orderのハッシュ。
242
+ #
243
+ def list_orders( )
244
+ tmp = {}
245
+ each_order_page {|result|
246
+ list = result.body.toutf8.scan( /<A href="[^"]*&meigaraId=([a-zA-Z0-9\/]*)[^"]*">[^<]*<\/A>\s*<BR>\s*受付時間:<BR>\s*([^<]*)<BR>\s*注文パターン:([^<]+)<BR>\s*([^<]+)<BR>\s*注文番号:(\d+)<BR>\s*注文価格:([^<]+)<BR>(?:\s*トレール幅:([^<]*)<BR>)?\s*約定価格:([^<]*)<BR>\s*数量\(未約定\):<BR>\s*(\d+)\(\d+\)単位<BR>\s*発注状況:([^<]*)<BR>/)
247
+ list.each {|i|
248
+ pair = to_pair( i[0] )
249
+ order_type = to_order_type_code(i[2])
250
+ trade_type = i[3] =~ /^新規.*/ ? SBIClient::FX::TRADE_TYPE_NEW : SBIClient::FX::TRADE_TYPE_SETTLEMENT
251
+ sell_or_buy = i[3] =~ /.*売\/.*/ ? SBIClient::FX::SELL : SBIClient::FX::BUY
252
+ execution_expression = if i[3] =~ /.*\/指値/
253
+ SBIClient::FX::EXECUTION_EXPRESSION_LIMIT_ORDER
254
+ elsif i[3] =~ /.*\/逆指値/
255
+ SBIClient::FX::EXECUTION_EXPRESSION_REVERSE_LIMIT_ORDER
256
+ else
257
+ SBIClient::FX::EXECUTION_EXPRESSION_MARKET_ORDER
258
+ end
259
+ order_no = i[4]
260
+ rate = i[5].to_f
261
+ trail_range = i[6].to_f if i[6]
262
+ count = i[8].to_i
263
+
264
+ tmp[order_no] = Order.new( order_no, trade_type, order_type,
265
+ execution_expression, sell_or_buy, pair, count, rate, trail_range, i[9])
266
+ }
267
+ false
268
+ }
269
+ return tmp
270
+ end
271
+
272
+ #
273
+ #===注文を行います。
274
+ #
275
+ #currency_pair_code:: 通貨ペアコード(必須)
276
+ #sell_or_buy:: 売買区分。SBIClient::FX::BUY,SBIClient::FX::SELLのいずれかを指定します。(必須)
277
+ #unit:: 取引数量(必須)
278
+ #options:: 注文のオプション。注文方法に応じて以下の情報を設定できます。
279
+ # - <b>成り行き注文</b>※未実装
280
+ # - なし
281
+ # - <b>通常注文</b> ※注文レートが設定されていれば通常取引となります。
282
+ # - <tt>:rate</tt> .. 注文レート(必須)
283
+ # - <tt>:execution_expression</tt> .. 執行条件。SBIClient::FX::EXECUTION_EXPRESSION_LIMIT_ORDER等を指定します(必須)
284
+ # - <tt>:expiration_type</tt> .. 有効期限。SBIClient::FX::EXPIRATION_TYPE_TODAY等を指定します(必須)
285
+ # - <tt>:expiration_date</tt> .. 有効期限が「日付指定(SBIClient::FX::EXPIRATION_TYPE_SPECIFIED)」の場合の有効期限をDateで指定します。(有効期限が「日付指定」の場合、必須)
286
+ # - <b>OCO注文</b> ※2つめの取引レートと2つめの取引種別が設定されていればOCO取引となります。
287
+ # - <tt>:rate</tt> .. 注文レート(必須)
288
+ # - <tt>:execution_expression</tt> .. 執行条件。SBIClient::FX::EXECUTION_EXPRESSION_LIMIT_ORDER等を指定します(必須)
289
+ # - <tt>:second_order_sell_or_buy</tt> .. 2つめの取引種別(必須) ※1つめの取引種別と同じ値にすると2つめの注文は逆指値になります。同じでなければ両者とも指値になります。
290
+ # - <tt>:second_order_rate</tt> .. 2つめの取引レート(必須)
291
+ # - <tt>:expiration_type</tt> .. 有効期限。SBIClient::FX::EXPIRATION_TYPE_TODAY等を指定します(必須)
292
+ # - <tt>:expiration_date</tt> .. 有効期限が「日付指定(SBIClient::FX::EXPIRATION_TYPE_SPECIFIED)」の場合の有効期限をDateで指定します。(有効期限が「日付指定」の場合、必須)
293
+ # - <b>IFD注文</b> ※決済取引の指定があればIFD取引となります。
294
+ # - <tt>:rate</tt> .. 注文レート(必須)
295
+ # - <tt>:execution_expression</tt> .. 執行条件。SBIClient::FX::EXECUTION_EXPRESSION_LIMIT_ORDER等を指定します(必須)
296
+ # - <tt>:expiration_type</tt> .. 有効期限。SBIClient::FX::EXPIRATION_TYPE_TODAY等を指定します(必須)
297
+ # - <tt>:expiration_date</tt> .. 有効期限が「日付指定(SBIClient::FX::EXPIRATION_TYPE_SPECIFIED)」の場合の有効期限をDateで指定します。(有効期限が「日付指定」の場合、必須)
298
+ # - <tt>:settle</tt> .. 決済取引の指定。マップで指定します。
299
+ # - <tt>:rate</tt> .. 決済取引の注文レート(必須)
300
+ # - <tt>:execution_expression</tt> .. 決済取引の執行条件。SBIClient::FX::EXECUTION_EXPRESSION_LIMIT_ORDER等を指定します(必須)
301
+ # - <b>IFD-OCO注文</b> ※決済取引の指定と逆指値レートの指定があればIFD-OCO取引となります。
302
+ # - <tt>:rate</tt> .. 注文レート(必須)
303
+ # - <tt>:execution_expression</tt> .. 執行条件。SBIClient::FX::EXECUTION_EXPRESSION_LIMIT_ORDER等を指定します(必須)
304
+ # - <tt>:expiration_type</tt> .. 有効期限。SBIClient::FX::EXPIRATION_TYPE_TODAY等を指定します(必須)
305
+ # - <tt>:expiration_date</tt> .. 有効期限が「日付指定(SBIClient::FX::EXPIRATION_TYPE_SPECIFIED)」の場合の有効期限をDateで指定します。(有効期限が「日付指定」の場合、必須)
306
+ # - <tt>:settle</tt> .. 決済取引の指定。マップで指定します。
307
+ # - <tt>:rate</tt> .. 決済取引の注文レート(必須)
308
+ # - <tt>:stop_order_rate</tt> .. 決済取引の逆指値レート(必須)
309
+ # - <b>トレール注文</b> ※トレール幅の指定があればトレール取引となります。
310
+ # - <tt>:rate</tt> .. 注文レート(必須) ※他の注文条件と違って<b>執行条件は逆指値で固定</b>です。
311
+ # - <tt>:expiration_type</tt> .. 有効期限。SBIClient::FX::EXPIRATION_TYPE_TODAY等を指定します(必須)
312
+ # - <tt>:expiration_date</tt> .. 有効期限が「日付指定(SBIClient::FX::EXPIRATION_TYPE_SPECIFIED)」の場合の有効期限をDateで指定します。(有効期限が「日付指定」の場合、必須)
313
+ # - <tt>:trail_range</tt> .. トレール幅(必須)
314
+ #戻り値:: SBIClient::FX::OrderResult
315
+ #
316
+ def order ( currency_pair_code, sell_or_buy, unit, options={} )
317
+
318
+ # 取り引き種別の判別とパラメータチェック
319
+ type = ORDER_TYPE_MARKET_ORDER
320
+ if ( options && options[:settle] != nil )
321
+ if ( options[:settle][:stop_order_rate] != nil)
322
+ # 逆指値レートと決済取引の指定があればIFD-OCO取引
323
+ raise "options[:settle][:rate] is required." unless options[:settle][:rate]
324
+ type = ORDER_TYPE_IFD_OCO
325
+ else
326
+ # 決済取引の指定のみがあればIFD取引
327
+ raise "options[:settle][:rate] is required." unless options[:settle][:rate]
328
+ raise "options[:settle][:execution_expression] is required." unless options[:settle][:execution_expression]
329
+ type = ORDER_TYPE_IFD
330
+ end
331
+ raise "options[:rate] is required." unless options[:rate]
332
+ raise "options[:execution_expression] is required." unless options[:execution_expression]
333
+ raise "options[:expiration_type] is required." unless options[:expiration_type]
334
+ elsif ( options && options[:rate] != nil )
335
+ if ( options[:second_order_rate] != nil && options[:second_order_sell_or_buy] != nil )
336
+ # 逆指値レートが指定されていればOCO取引
337
+ raise "options[:execution_expression] is required." unless options[:execution_expression]
338
+ type = ORDER_TYPE_OCO
339
+ elsif ( options[:trail_range] != nil )
340
+ # トレール幅が指定されていればトレール取引
341
+ type = ORDER_TYPE_TRAIL
342
+ else
343
+ # そうでなければ通常取引
344
+ type = ORDER_TYPE_NORMAL
345
+ raise "options[:execution_expression] is required." unless options[:execution_expression]
346
+ end
347
+ raise "options[:expiration_type] is required." unless options[:expiration_type]
348
+ else
349
+ # 成り行き
350
+ type = ORDER_TYPE_MARKET_ORDER
351
+ end
352
+
353
+ #新規注文
354
+ result = link_click( "2" )
355
+ SBIClient::Client.error( result ) if result.forms.empty?
356
+ form = result.forms.first
357
+ form.meigaraId = currency_pair_code.to_s.insert(3, "/").to_sym
358
+ form.radiobuttons_with("urikai").each {|b|
359
+ b.check if b.value == ( sell_or_buy == SBIClient::FX::BUY ? "1" : "-1" )
360
+ }
361
+
362
+ # 詳細設定画面へ
363
+ result = @client.submit(form)
364
+ SBIClient::Client.error( result ) if result.forms.empty?
365
+ form = result.forms.first
366
+ case type
367
+ when ORDER_TYPE_MARKET_ORDER
368
+ # 成り行き
369
+ form.sikkouJyouken = "0"
370
+ when ORDER_TYPE_NORMAL
371
+ # 指値
372
+ set_expression( form, options[:execution_expression] )
373
+ set_rate(form, options[:rate])
374
+ set_expiration( form, options ) # 有効期限
375
+ when ORDER_TYPE_OCO
376
+ # OCO
377
+ form.order = "3"
378
+ result = @client.submit(form, form.buttons.find {|b| b.value=="選択" } )
379
+ form = result.forms.first
380
+ set_expression( form, options[:execution_expression] )
381
+ set_rate(form, options[:rate])
382
+ form["urikai2"] = ( options[:second_order_sell_or_buy] == SBIClient::FX::BUY ? "1" : "-1" )
383
+ result = @client.submit(form, form.buttons.find {|b| b.value=="次へ" })
384
+ form = result.forms.first
385
+ SBIClient::Client.error( result ) unless result.body.toutf8 =~ /maisuu/
386
+ set_rate(form, options[:second_order_rate], "2")
387
+ set_expiration( form, options ) # 有効期限
388
+ when ORDER_TYPE_IFD
389
+ # IFD
390
+ form.order = "2"
391
+ result = @client.submit(form, form.buttons.find {|b| b.value=="選択" })
392
+ form = result.forms.first
393
+ set_expression( form, options[:execution_expression] )
394
+ set_rate(form, options[:rate], "3")
395
+ set_expression( form, options[:settle][:execution_expression], "sikkouJyouken2" )
396
+ set_rate(form, options[:settle][:rate], "1")
397
+ set_expiration( form, options ) # 有効期限
398
+ when ORDER_TYPE_IFD_OCO
399
+ form.order = "4"
400
+ result = @client.submit(form, form.buttons.find {|b| b.value=="選択" })
401
+ form = result.forms.first
402
+ set_expression( form, options[:execution_expression] )
403
+ set_rate(form, options[:rate], "3")
404
+ set_rate(form, options[:settle][:rate], "1")
405
+ set_rate(form, options[:settle][:stop_order_rate], "2")
406
+ set_expiration( form, options ) # 有効期限
407
+ when ORDER_TYPE_TRAIL
408
+ form.order = "6"
409
+ result = @client.submit(form, form.buttons.find {|b| b.value=="選択" })
410
+ form = result.forms.first
411
+ set_rate(form, options[:rate], "1")
412
+ set_trail(form, options[:trail_range])
413
+ set_expiration( form, options ) # 有効期限
414
+ else
415
+ raise "unknown order type."
416
+ end
417
+ form.maisuu = unit.to_s
418
+ form["postTorihikiPs"] = @order_password
419
+
420
+ # 確認画面へ
421
+ result = @client.submit( form, form.buttons.find {|b| b.value=="注文確認" } )
422
+ SBIClient::Client.error( result ) unless result.body.toutf8 =~ /注文確認/
423
+
424
+ result = @client.submit(result.forms.first)
425
+ SBIClient::Client.error( result ) unless result.body.toutf8 =~ /注文番号\:[^\d]+(\d+)/
426
+ return OrderResult.new( $1 )
427
+ end
428
+
429
+ #
430
+ #=== 成り行きで決済注文を行います。
431
+ #
432
+ #position_id:: 決済する建玉番号
433
+ #count:: 取引数量
434
+ #<b>戻り値</b>:: なし
435
+ def settle ( position_id, count )
436
+
437
+ result = link_click( "6" )
438
+ result = link_click( "2", result.links )
439
+
440
+ #IDを解析
441
+ raise "illegal position_id. position_id=#{position_id}" unless position_id=~ /^(1|\-1)_([A-Z]{3})([A-Z]{3})_(\d*)$/
442
+ sell_or_buy = $1
443
+ pair = "#{$2}/#{$3}"
444
+ id = $4
445
+
446
+ # 建玉一覧
447
+ form = result.forms.find{|f|
448
+ f.name == "orderForm"
449
+ }
450
+ SBIClient::Client.error( result ) unless form
451
+ form.meigaraId = pair
452
+ result = @client.submit(form)
453
+ link = result.links.find {|l|
454
+ l.href =~ /syoukaiTatigyoku.aspx\?.*&urikai=#{sell_or_buy}/
455
+ }
456
+ raise "position not found. position_id=#{position_id}" unless link
457
+ result = @client.click(link)
458
+
459
+ # ポジション一覧
460
+ link = nil
461
+ each_page( result ) {|r|
462
+ link = r.links.find {|l|
463
+ l.href =~ /syoukaiTatigyoku.aspx\?.*&tId=#{id}/
464
+ }
465
+ link != nil
466
+ }
467
+ raise "position not found. position_id=#{position_id}" unless link
468
+ result = @client.click(link)
469
+ form = result.forms.find{|f|
470
+ f.name == "kessaiForm"
471
+ }
472
+ result = @client.submit(form, form.buttons.first)
473
+ SBIClient::Client.error( result ) if result.forms.empty?
474
+ form = result.forms.first
475
+ form.sikkouJyouken = "0"
476
+ form.maisuu = count
477
+ form["postTorihikiPs"] = @order_password
478
+
479
+ # 確認画面へ
480
+ result = @client.submit( form, form.buttons.find {|b| b.value=="注文確認" } )
481
+ SBIClient::Client.error( result ) unless result.body.toutf8 =~ /注文確認/
482
+
483
+ result = @client.submit(result.forms[1], form.buttons.first )
484
+ SBIClient::Client.error( result ) unless result.body.toutf8 =~ /注文受付/
485
+ end
486
+
487
+ #
488
+ #=== 注文をキャンセルします。
489
+ #
490
+ #order_no:: 注文番号
491
+ #戻り値:: なし
492
+ #
493
+ def cancel_order( order_no )
494
+
495
+ raise "order_no is nil." unless order_no
496
+
497
+ # 注文一覧
498
+ link = nil
499
+ each_order_page {|result|
500
+ SBIClient::Client.error( result ) if result.links.empty?
501
+ # 対象となる注文をクリック
502
+ link = result.links.find {|l|
503
+ l.href =~ /[^"]*Id=([\d]+)[^"]*/ && $1 == order_no
504
+ }
505
+ link != nil
506
+ }
507
+ raise "illegal order_no. order_no=#{order_no}" unless link
508
+ result = @client.click(link)
509
+ SBIClient::Client.error( result ) if result.forms.empty?
510
+
511
+ # キャンセル
512
+ form = result.forms.first
513
+ form.radiobuttons_with("tkF").each{|b|
514
+ b.check if b.value == "kesi"
515
+ }
516
+ result = @client.submit(form)
517
+ SBIClient::Client.error( result ) unless result.body.toutf8 =~ /注文確認/
518
+ form = result.forms.first
519
+ form.ToriPs = @order_password
520
+ result = @client.submit(form, form.buttons.find {|b| b.value=="注文取消" })
521
+ SBIClient::Client.error( result ) unless result.body.toutf8 =~ /取消を受付致しました/
522
+
523
+ end
524
+
525
+ #
526
+ #=== 建玉一覧を取得します。
527
+ #
528
+ #戻り値:: 建玉IDをキーとするSBIClient::FX::Positionのハッシュ。
529
+ #
530
+ def list_positions
531
+ result = link_click( "6" )
532
+ result = link_click( "2", result.links )
533
+
534
+ tmp = {}
535
+ each_page( result ) {|res|
536
+ links = res.links.find_all {|l|
537
+ l.href =~ /[^"]syoukai\/syoukaiTatigyoku.aspx\?[^"]*/
538
+ }
539
+ links.each {|link|
540
+ re = @client.click( link )
541
+ each_page( re ) {|r|
542
+ positions = r.body.toutf8.scan(/\<A HREF="[^"]*&urikai=([^"&]*)&[^"]*meigaraId=([^"&]*)&[^"]*tId=([^"&]*)[^"]*">\s*([\/\s\:\d]*)<\/A>\s*<BR>\s*数量[^>]*>\s*(\d+)\((\d*)\)[^\:]*\:\s*([\d\.]+)<BR>[^>]*>[^>]*>([\d\+\-\.\,]+)</)
543
+ positions.each {|i|
544
+ sell_or_buy = i[0] == "1" ? SBIClient::FX::SELL : SBIClient::FX::BUY
545
+ pair = to_pair( i[1] )
546
+ position_id = "#{i[0]}_#{pair}_#{i[2]}"
547
+ date = DateTime.parse(i[3], "%Y/%m/%d %H:%M:%S")
548
+ count = i[4].to_i
549
+ rate = i[6].to_f
550
+ profit_or_loss = i[7].gsub(/[\+\,]/, "").to_i
551
+ tmp[position_id] = Position.new(position_id, pair, sell_or_buy, count, rate, profit_or_loss, date )
552
+ }
553
+ false
554
+ }
555
+ }
556
+ false
557
+ }
558
+ return tmp
559
+ end
560
+
561
+ #===ログアウトします。
562
+ def logout
563
+ link_click( "*" )
564
+ end
565
+
566
+ private
567
+ # 注文一覧ページをブロックがtrueを返すまで列挙します。
568
+ def each_order_page( &block )
569
+ each_page( link_click( "4" ), &block)
570
+ end
571
+ # 複数ページからなる一覧の各ページを、ブロックがtrueを返すまで列挙します。
572
+ #result:: ページの1つ
573
+ def each_page( result )
574
+ pages = result.links.find_all {|l| l.text =~ /\d+/}
575
+ return if yield result
576
+ pages.each {|link|
577
+ result = @client.click( link )
578
+ break if yield result
579
+ }
580
+ end
581
+
582
+ # フォームに執行条件を設定します。
583
+ def set_expression( form, exp, key="sikkouJyouken" ) #:nodoc:
584
+ form[key] = exp == SBIClient::FX::EXECUTION_EXPRESSION_REVERSE_LIMIT_ORDER ? "2" : "1" #指値/逆指値
585
+ end
586
+ # フォームにトレール幅を設定します。
587
+ def set_trail(form, trail, index=1) #:nodoc:
588
+ raise "illegal trail. trail=#{trail}" unless trail.to_s =~ /(\d+)(\.\d*)?/
589
+ form["trail#{index}_1"] = $1
590
+ form["trail#{index}_2"] = $2[1..$2.length] if $2
591
+ end
592
+ # フォームにレートを設定します。
593
+ def set_rate(form, rate, index=1) #:nodoc:
594
+ raise "illegal rate. rate=#{rate}" unless rate.to_s =~ /(\d+)(\.\d*)?/
595
+ form["sasine#{index}_1"] = $1
596
+ form["sasine#{index}_2"] = $2[1..$2.length] if $2
597
+ end
598
+
599
+ # 注文種別を注文種別コードに変換します。
600
+ def to_order_type_code( order_type )
601
+ return case order_type
602
+ when "成行"
603
+ SBIClient::FX::ORDER_TYPE_MARKET_ORDER
604
+ when "通常"
605
+ SBIClient::FX::ORDER_TYPE_NORMAL
606
+ when "OCO1"
607
+ SBIClient::FX::ORDER_TYPE_OCO
608
+ when "OCO2"
609
+ SBIClient::FX::ORDER_TYPE_OCO
610
+ when "IFD1"
611
+ SBIClient::FX::ORDER_TYPE_IFD
612
+ when "IFD2"
613
+ SBIClient::FX::ORDER_TYPE_IFD
614
+ when "IFDOCO1"
615
+ SBIClient::FX::ORDER_TYPE_IFD_OCO
616
+ when "IFDOCO2"
617
+ SBIClient::FX::ORDER_TYPE_IFD_OCO
618
+ when "トレール"
619
+ SBIClient::FX::ORDER_TYPE_TRAIL
620
+ else
621
+ raise "illegal order_type. order_type=#{order_type}"
622
+ end
623
+ end
624
+
625
+ # 有効期限を設定します。
626
+ def set_expiration( form, options ) #:nodoc:
627
+ date = nil
628
+ case options[:expiration_type]
629
+ when SBIClient::FX::EXPIRATION_TYPE_TODAY
630
+ # デフォルトを使う
631
+ return
632
+ when SBIClient::FX::EXPIRATION_TYPE_WEEK_END
633
+ date = DateTime.now + 7
634
+ when SBIClient::FX::EXPIRATION_TYPE_SPECIFIED
635
+ raise "options[:expiration_date] is required." unless options[:expiration_date]
636
+ date = options[:expiration_date]
637
+ else
638
+ return
639
+ end
640
+ return unless date
641
+ form["yuukou_kigen_date"] = date.strftime("%Y/%m/%d")
642
+ if date.kind_of?(DateTime)
643
+ form["yuukou_kigen_jikan"] = sprintf("%02d", date.hour )
644
+ form["yuukou_kigen_fun"] = sprintf("%02d", date.min )
645
+ end
646
+ end
647
+
648
+ #レートページを列挙します
649
+ def each_rate_page( &block ) #:nodoc:
650
+ result = link_click( "1" )
651
+ block.call( result ) if block_given?
652
+ result.links.each {|i|
653
+ next unless i.text =~ /^\d+$/
654
+ res = @client.click( i )
655
+ block.call( res ) if block_given?
656
+ }
657
+ end
658
+
659
+ #ページからレート情報を収集します
660
+ def collect_rate( page, map ) #:nodoc:
661
+ tokens = page.body.toutf8.scan( RATE_REGEX )
662
+ tokens.each {|t|
663
+ next unless t[0] =~ /\&meigaraId\=([A-Z\/]+)&/
664
+ pair = to_pair( $1 )
665
+ swap = @swaps[pair]
666
+ rate = FxSession.convert_rate t[2]
667
+ if ( rate && swap )
668
+ map[pair] = Rate.new( pair, rate[0], rate[1], swap.sell_swap, swap.buy_swap )
669
+ end
670
+ }
671
+ end
672
+ RATE_REGEX = /◇<A([^>]*)>([^<]*)<\/A>\s*<BR>\s*<CENTER>\s*<B>\s*([\d\-_\.]+)\s*<\/B>\s*<\/CENTER>/
673
+
674
+ #12.34-12.35 形式の文字列をbidレート、askレートに変換します。
675
+ def self.convert_rate( str ) #:nodoc:
676
+ if str =~ /([\d.]+)\-([\d.]+)/
677
+ return [$1.to_f,$2.to_f]
678
+ end
679
+ end
680
+
681
+ #ページからスワップ情報を収集します
682
+ def collect_swap( page, map ) #:nodoc:
683
+ page.links.each {|i|
684
+ next unless i.href =~ /\&meigaraId\=([A-Z\/]+)&/
685
+ pair = to_pair( $1 )
686
+ res = @client.click( i )
687
+ next unless res.body.toutf8 =~ /SW売\/買\(円\)\:<BR>\s*([\-\d]*)\/([\-\d]*)\s*</
688
+ map[pair] = Swap.new( pair, $1.to_i, $2.to_i )
689
+ }
690
+ end
691
+
692
+ # "USD/JPY"を:USDJPYのようなシンボルに変換します。
693
+ def to_pair( str ) #:nodoc:
694
+ str.gsub( /\//, "" ).to_sym
695
+ end
696
+
697
+ def link_click( no, links=@links )
698
+ link = links.find {|i|
699
+ i.attributes["accesskey"] == no
700
+ }
701
+ raise "link isnot found. accesskey=#{no}" unless link
702
+ @client.click( link )
703
+ end
704
+ end
705
+
706
+ # オプション
707
+ attr :options, true
708
+
709
+ #=== スワップ
710
+ Swap = Struct.new(:pair, :sell_swap, :buy_swap)
711
+ #=== レート
712
+ Rate = Struct.new(:pair, :bid_rate, :ask_rate, :sell_swap, :buy_swap )
713
+ #===注文結果
714
+ OrderResult = Struct.new(:order_no )
715
+ #===注文
716
+ Order = Struct.new(:order_no, :trade_type, :order_type, :execution_expression, \
717
+ :sell_or_buy, :pair, :count, :rate, :trail_range, :order_state )
718
+ #===建玉
719
+ Position = Struct.new(:position_id, :pair, :sell_or_buy, :count, :rate, :profit_or_loss, :date )
720
+ end
721
+ end
722
+
723
+
724
+