sbiclient 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
+