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/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
|
+
|