sbiclient 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README +30 -0
- data/lib/jiji_plugin.rb +150 -0
- data/lib/sbiclient.rb +724 -0
- data/sample/common.rb +28 -0
- data/sample/sample_ifd_oco_order.rb +80 -0
- data/sample/sample_ifd_order.rb +80 -0
- data/sample/sample_list_positions.rb +17 -0
- data/sample/sample_list_rates.rb +16 -0
- data/sample/sample_oco_order.rb +126 -0
- data/sample/sample_order.rb +64 -0
- data/sample/sample_settle.rb +18 -0
- data/sample/sample_trail_order.rb +42 -0
- data/spec/cancel_order_spec.rb +62 -0
- data/spec/common.rb +20 -0
- data/spec/ifd_oco_order_spec.rb +59 -0
- data/spec/ifd_order_spec.rb +110 -0
- data/spec/jiji_plugin_spec!.rb +72 -0
- data/spec/limit_order_spec.rb +93 -0
- data/spec/list_orders_spec.rb +51 -0
- data/spec/market_order_spec!.rb +47 -0
- data/spec/oco_order_spec.rb +307 -0
- data/spec/order_error_spec.rb +405 -0
- data/spec/trail_order_spec.rb +51 -0
- metadata +106 -0
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
|
+
|