// ==UserScript== // @name SBI SECURITIES Future Trade Support // @namespace http://m4i.jp/ // @include https://depindex.sbisec.co.jp/cfront/servlet/cfront.OrderController* // @require http://code.jquery.com/jquery-1.2.6.min.js // ==/UserScript== var Util = { memoize: function(property, object/* = window */) { if (property.constructor === Array) { for (var i = 0, l = property.length; i < l; i++) { arguments.callee(property[i], object); } } else { object = object || window; var original = object[property]; object[property] = function() { if (typeof arguments.callee.memo === 'undefined') { arguments.callee.memo = original.call(object); } return arguments.callee.memo; }; } }, raiseError: function() { alert('UserScript: SBI SECURITIES Future Trade Support\n\nERROR!'); throw true; }, getUpperPrice: function(price) { switch (true) { case Page.isLarge(): return price + 10; case Page.isMini(): return price + 5; case Page.isOption(): if (price < 20) return price + 1; if (price < 1000) return price + 5; return price + 10; } Util.raiseError(); }, getLowerPrice: function(price) { switch (true) { case Page.isLarge(): return price - 10; case Page.isMini(): return price - 5; case Page.isOption(): if (price > 1000) return price - 10; if (price > 20) return price - 5; return price - 1; } Util.raiseError(); }, }; var Page = { isFuture: function() { return this.isFutureTop() || this.isLarge() || this.isMini(); }, isFutureTop: function() { var ml_type_inputs = this._getMlTypeInputs(); return ml_type_inputs.length === 2 && ml_type_inputs[0].value === '1' && ml_type_inputs[1].value === '2'; }, isLarge: function() { var ml_type_inputs = this._getMlTypeInputs(); return ml_type_inputs.length === 1 && ml_type_inputs[0].value === '1'; }, isMini: function() { var ml_type_inputs = this._getMlTypeInputs(); return ml_type_inputs.length === 1 && ml_type_inputs[0].value === '2'; }, isOption: function() { return this.isOptionTop() || this.isCall() || this.isPut(); }, isOptionTop: function() { var put_call_inputs = this._getPutCalInputs(); return put_call_inputs.length === 2 && put_call_inputs[0].value === '2' && put_call_inputs[1].value === '1'; }, isCall: function() { var put_call_inputs = this._getPutCalInputs(); return put_call_inputs.length === 1 && put_call_inputs[0].value === '2'; }, isPut: function() { var put_call_inputs = this._getPutCalInputs(); return put_call_inputs.length === 1 && put_call_inputs[0].value === '1'; }, isCorrecting: function() { return document.getElementsByName('CORRECT_PRICE').length > 0; }, hasKehaiIta: function() { return this.isLarge() || this.isMini() || this.isCall() || this.isPut(); }, _getMlTypeInputs: function() { return document.getElementsByName('ML_TYPE'); }, _getPutCalInputs: function() { return document.getElementsByName('PUT_CALL'); }, }; for (var property in Page) Util.memoize(property, Page); var Radioize = { ORDER_PATTERN: { TSUJO: '0', PAIR: '1', STEP: '2', STEP_PAIR: '3', }, ORDER_CONDITION_GROUP: { SASHINE: ['1', '2', '3', '4', '5'], NARIYUKI: ['6', '7', '8'], }, orderPattern: function() { this._radioize('ORDER_PATTERN', 4, unsafeWindow.PatternOnChange); }, orderCondition: function() { var order_pattern_select = document.getElementsByName('ORDER_PATTERN')[0]; if (!order_pattern_select) return; var NARIYUKI = this.ORDER_CONDITION_GROUP.NARIYUKI; var priceInput = document.getElementsByName(Page.isCorrecting() ? 'CORRECT_PRICE' : 'PRICE')[0]; var previousPrice = ''; var disablePrice = function(select) { if ($.inArray(select.value, NARIYUKI) == -1) { if (!priceInput.value) { priceInput.value = previousPrice; } } else { if (priceInput.value) { previousPrice = priceInput.value; } priceInput.value = ''; } }; var orderConditionNamePrefix = Page.isCorrecting() ? 'CORRECT_ORDER_CONDITION' : 'ORDER_CONDITION'; var order_pattern = $(order_pattern_select).val(); this._radioize(orderConditionNamePrefix, order_pattern == this.ORDER_PATTERN.TSUJO ? 4 : 1, order_pattern == this.ORDER_PATTERN.PAIR ? unsafeWindow.PatternOnChange : disablePrice ); this._radioize(orderConditionNamePrefix + '_IFDONE', 1); this._radioize(orderConditionNamePrefix + '_IFDOCO1', 1, unsafeWindow.PatternOnChange); }, _radioize: function(name, columns, onClickCallback) { var select = document.getElementsByName(name)[0]; if (!select) return; name = name.toLowerCase() + '_'; onClickCallback = onClickCallback || function() {}; $(select).css({ position: 'absolute', visibility: 'hidden', }); var container = select.parentNode; // ずれないように余分な空白を削除 this._removeTextChildNodes(container); $(select).find('option').each(function(i) { if (columns && i != 0 && i % columns == 0) { container.appendChild(document.createElement('br')); } var id = name + this.value; var radio = document.createElement('input'); radio.type = 'radio'; radio.id = id; radio.name = name; radio.value = this.value; radio.checked = this.selected; $(radio).click(function() { if (this.value == $(select).val()) return; $(select).val(this.value); onClickCallback(select); }); var label = document.createElement('label'); label.setAttribute('for', id); $(label).text($(this).text()); container.appendChild(radio); container.appendChild(label); }); }, _removeTextChildNodes: function(element) { for (var i = element.childNodes.length - 1; i >= 0; i--) { var node = element.childNodes[i]; if (node.nodeType == 3 && /^\s*$/.test(node.data)) { element.removeChild(node); } } }, }; function setPriceByKehaine() { if (!Page.hasKehaiIta()) return; var NUMBER_OF_APPEND_ROWS = 9; var NUMBER_OF_MAX_MIDDLE_ROWS = 4; var tbody = $('table[width=235] tbody').eq(0); if (tbody.find('tr:eq(0) div:eq(1)').text() != '気配値') Util.raiseError(); tbody.find('div').css({ lineHeight: '1', fontSize: 'xx-small', }); var trs = tbody.find('tr:gt(1)'); var prices = trs.find('div:eq(1)').map( function() toNumberFromFormattedNumber($(this).text())); var rowIndex = { ask: { max: 0 }, bit: { min: trs.length - 1 } }; rowIndex.ask.min = Math.floor(rowIndex.bit.min / 2); rowIndex.bit.max = Math.ceil(rowIndex.bit.min / 2); var lack = { ask: false, bit: false }; // 気配値が全く存在しないければ中断 if (!prices[rowIndex.ask.min] && !prices[rowIndex.bit.max]) return; // ask を埋める for (var i = rowIndex.ask.min; i >= rowIndex.ask.max; i--) { if (prices[i]) continue; prices[i] = Util.getUpperPrice(prices[i + 1]); var divs = trs.eq(i).find('div'); divs.eq(1).text(formatNumber(prices[i])); divs.eq(0).text('0'); lack.ask = true; } // bit を埋める for (var i = rowIndex.bit.max; i <= rowIndex.bit.min; i++) { if (prices[i]) continue; prices[i] = Util.getLowerPrice(prices[i - 1]); if (prices[i] <= 0) break; var divs = trs.eq(i).find('div'); divs.eq(1).text(formatNumber(prices[i])); divs.eq(2).text('0'); lack.bit = true; } // 追加行の skelton を作成 var skelton = { ask: $(trs[rowIndex.ask.max].cloneNode(true)), middle: $(tbody.find('tr')[1].cloneNode(true)), bit: $(trs[rowIndex.bit.min].cloneNode(true)), }; skelton.ask.find('div:last').text(''); skelton.middle.find('div').text('0'); skelton.bit.find('div:first').text(''); // ask と bit の間を追加 var ask = prices[rowIndex.ask.min], bit = prices[rowIndex.bit.max]; var insertBase = trs.eq(rowIndex.ask.min); var numberOfAppendRows = { ask: NUMBER_OF_APPEND_ROWS, bit: NUMBER_OF_APPEND_ROWS, }; for (var i = 0; i < NUMBER_OF_MAX_MIDDLE_ROWS; i++) { ask = Util.getLowerPrice(ask); if (ask === bit) break; skelton.middle.find('div:eq(1)').text(formatNumber(ask)); insertBase.after(skelton.middle[0].cloneNode(true)); insertBase = insertBase.next(); numberOfAppendRows.ask--; bit = Util.getUpperPrice(bit); if (bit === ask) break; skelton.middle.find('div:eq(1)').text(formatNumber(bit)); insertBase.after(skelton.middle[0].cloneNode(true)); numberOfAppendRows.bit--; } // ask と bit の外側を追加 var appendRows = function(ask_or_bit, price, getNextPrice, kehaineSu, selector) { for (var i = 0; i < numberOfAppendRows[ask_or_bit]; i++) { price = getNextPrice(price); if (price <= 0) break; var tr = $(skelton[ask_or_bit][0].cloneNode(true)); tr.find('div:eq(1)').text(formatNumber(price)); tr.find('div' + kehaineSu).text(lack[ask_or_bit] ? '0' : '----'); tbody.find(selector).after(tr); } }; appendRows('ask', prices[rowIndex.ask.max], Util.getUpperPrice, ':first', 'tr:eq(1)'); appendRows('bit', prices[rowIndex.bit.min], Util.getLowerPrice, ':last', 'tr:last'); var priceNamePrefix = Page.isCorrecting() ? 'CORRECT_PRICE' : 'PRICE'; var priceInput; if (priceInput = document.getElementsByName(priceNamePrefix)[0]) { var priceInputs = []; priceInputs[0] = $(priceInput); if (priceInput = document.getElementsByName(priceNamePrefix + '_OCO')[0]) { priceInputs[1] = priceInputs[0]; priceInputs[2] = $(priceInput); } else if (priceInput = document.getElementsByName(priceNamePrefix + '_IFDONE')[0]) { priceInputs[1] = priceInputs[0]; priceInputs[2] = $(priceInput); } else if (priceInput = document.getElementsByName(priceNamePrefix + '_IFDOCO1')[0]) { priceInputs[1] = $(priceInput); priceInputs[2] = $(document.getElementsByName(priceNamePrefix + '_IFDOCO2')[0]); } else { priceInputs[1] = priceInputs[0]; priceInputs[2] = priceInputs[0]; } var setPrice = function(price, index) { priceInputs[index].val(price); }; var onMouseOver = function(index) { $(this).css('background-color', '#fc9'); priceInputs[index].css('background-color', '#fc9'); }; var onMouseOut = function(index) { $(this).css('background-color', ''); priceInputs[index].css('background-color', ''); }; tbody.find('tr:gt(1)').each(function() { var price = toNumberFromFormattedNumber($(this).find('div:eq(1)').text()); $(this).find('td').each(function(i) { $(this) .click(function() setPrice(price, i)) .mouseover(function() onMouseOver.call(this, i)) .mouseout(function() onMouseOut.call(this, i)) .css('cursor', 'pointer'); }); }); } function toNumberFromFormattedNumber(string) { return Number(string.replace(/\D+/g, '')); } function formatNumber(number) { return String(number).replace(/\d(?=(?:\d{3})+$)/g, '$&,'); } } function setQuantity() { var quantity_input = document.getElementsByName('QUANTITY')[0]; if (!quantity_input) return; quantity_input.value = GM_getValue('quantity') || 1; $(quantity_input).change(function() GM_setValue('quantity', this.value)); } function setPassword() { var trade_pwd_input = document.getElementsByName('trade_pwd')[0]; if (!trade_pwd_input) return; var accountNumber = getAccountNumber(); if (!accountNumber) return; var password; var cryptedPassword = GM_getValue('password'); if (cryptedPassword) { password = decryptPassword(cryptedPassword, accountNumber); } else { var message = [ 'UserScript - SBI SECURITIES Future Trade Support', '', '取引パスワードを入力してください', '', '入力した取引パスワードは口座番号を鍵としてごく簡単な暗号化がなされ prefs.js に保存されます。', 'よって、口座番号と prefs.js を入手し、この UserScript を読むことができれば取引パスワードを知ることが可能です。', 'ご利用には十分ご注意ください。', 'なお保存されたパスワードの削除は about:config から行ってください。', ].join('\n'); if (!(password = prompt(message))) return; GM_setValue('password', cryptPassword(password, accountNumber)); } trade_pwd_input.value = password; function getAccountNumber() { if (!unsafeWindow.top.indicator) return; return $(unsafeWindow.top.indicator.document) .find('font:eq(0)[color=#666666]').text(); } function cryptPassword(password, accountNumber) { var key = makeKey(accountNumber); return uneval(password.split('').map(function(c, i) c.charCodeAt(0) ^ key.charCodeAt(i % key.length))); } function decryptPassword(cryptedPassword, accountNumber) { var key = makeKey(accountNumber); return String.fromCharCode.apply(null, eval(cryptedPassword) .map(function(c, i) c ^ key.charCodeAt(i % key.length))); } function makeKey(accountNumber) { return accountNumber.replace(/\D+/g, ''); } } function checkSkipEstimate() { var skip_estimate_checkbox = document.getElementsByName('skip_estimate')[0]; if (!skip_estimate_checkbox) return; skip_estimate_checkbox.checked = true; } // 注文パターンをラジオボタンに Radioize.orderPattern(); // 執行条件をラジオボタンに + 成行で価格リセット Radioize.orderCondition(); // 価格を気配値をクリックしてセット setPriceByKehaine(); // 数量に前回入力した値をセット setQuantity(); // 取引パスワードをセット setPassword(); //「注文確認画面を省略」を常にチェック checkSkipEstimate();