TinyGrail AutoBid

小圣杯自动挂单(买单)

// ==UserScript==
// @name         TinyGrail AutoBid
// @namespace    https://github.com/bangumi/scripts/tree/master/liaune
// @version      0.1.3
// @description  小圣杯自动挂单(买单)
// @author       Liaune
// @include     /^https?://(bgm\.tv|bangumi\.tv|chii\.in)/(user|character|rakuen\/topic\/crt).*
// @grant        GM_addStyle
// ==/UserScript==
let api = 'https://tinygrail.com/api/';

function getData(url) {
	if (!url.startsWith('http')) url = api + url;
	return new Promise((resovle, reject) => {
		$.ajax({
			url: url,
			type: 'GET',
			xhrFields: { withCredentials: true },
			success: res => {resovle(res)},
			error: err => {reject(err)}
		});
	});
}

function postData(url, data) {
	let d = JSON.stringify(data);
	if(!url.startsWith('http')) url = api + url;
	return new Promise((resovle, reject) => {
		$.ajax({
			url: url,
			type: 'POST',
			contentType: 'application/json',
			data: d,
			xhrFields: { withCredentials: true },
			success: res => {resovle(res)},
			error: err => {reject(err)}
		});
	});
}

function formatBidPrice(price){
	return (price - Math.round(price))>=0 ? Math.round(price) : Math.round(price)-0.5;
}
function formatAskPrice(price){
    if(Number.isInteger(parseFloat(price))) return price;
	else return (price - Math.floor(price))>0.5 ? Math.ceil(price) : Math.floor(price)+0.5;
}

function maxFloat(price){
	return (price*(Math.pow(2,52)-1)/Math.pow(2,52));
}

function remove_empty(array){
	let arr = [];
	for(let i = 0; i < array.length; i++){
		if(array[i]) arr.push(array[i]);
	}
	return arr;
}

let bidList = JSON.parse(localStorage.getItem('TinyGrail_bidList')) || [];
let followList = JSON.parse(localStorage.getItem('TinyGrail_followList')) || {"user":'',"charas":[], "auctions":[]};

async function autoBid(charas){
	//closeDialog();
	var dialog = `<div id="TB_overlay" class="TB_overlayBG TB_overlayActive"></div>
<div id="TB_window" class="dialog" style="display:block;max-width:640px;min-width:400px;">
<div class="info_box">
<div class="title">自动挂单检测中</div>
<div class="result" style="max-height:500px;overflow:auto;"></div>
</div>
<a id="TB_closeWindowButton" title="Close">X关闭</a>
</div>
</div>`;
	if(!$('#TB_window').length) $('body').append(dialog);
	$('#TB_closeWindowButton').on('click', closeDialog);
	$('#TB_overlay').on('click', closeDialog);
	for (let i = 0; i < charas.length; i++) {
		$('.info_box .title').text(`自动挂单检测中 ${i+1} / ${charas.length}`);
		let charaId = charas[i].charaId;
		await getData(`chara/user/${charaId}`).then((d)=>{
			let Bids = d.Value.Bids;
			let Asks = d.Value.Asks;
			let Amount = d.Value.Amount;
            let BidHistory = d.Value.BidHistory;
			let [myBidPrice, myBidAmount] = getBids(d.Value.Bids,0);
			let [myAskPrice, myAskAmount] = getAsks(d.Value.Asks,0);
			let [myBidPrice_ice, myBidAmount_ice] = getBids(d.Value.Bids,1);
			let [myAskPrice_ice, myAskAmount_ice] = getAsks(d.Value.Asks,1);
			let time = new Date().toLocaleTimeString();
			$('.info_box .result').prepend(`<div class="row">${time} check #${charaId} ${charas[i].name}</div>`);

			function getBids(Bids,is_ice){ //获取自己最高买价和数量
				let [myBidPrice, myBidAmount] = [0, 0];
				for(let i = 0; i < Bids.length; i++) {
					if(Bids[i].Price > myBidPrice && Bids[i].Type==is_ice) [myBidPrice, myBidAmount] = [Bids[i].Price, Bids[i].Amount];
				}
				return [myBidPrice, myBidAmount];
			}
			function getAsks(Asks,is_ice){ //获取自己最低卖价和数量
				let [myAskPrice, myAskAmount] = [9999, 0];
				for(let i = 0; i < Asks.length; i++) {
					if(Asks[i].Price < myAskPrice && Asks[i].Type==is_ice) myAskPrice = Asks[i].Price;
				}
                for(let i = 0; i < Asks.length; i++) {
					if(Asks[i].Price == myAskPrice && Asks[i].Type==is_ice) myAskAmount += Asks[i].Amount;
				}
				return [myAskPrice, myAskAmount];
			}
			function cancelBid(type,is_ice){ //取消最高买单
				let [myBidPrice, myBidAmount, tid] = [0, 0, 0];
				for(let i = 0; i < Bids.length; i++) {
					if(Bids[i].Price > myBidPrice && Bids[i].Type==is_ice) [myBidPrice,myBidAmount,tid] = [Bids[i].Price,Bids[i].Amount, Bids[i].Id];
				}
				if(tid){
					postData(`chara/bid/cancel/${tid}`, null).then((d)=>{
						console.log(`${time} ${type}: 取消买单#${charaId} ${myBidPrice}*${myBidAmount}`);
						$('.info_box .result').prepend(`<div class="row">${time} ${type}: 取消买单 #${charaId} ${charas[i].name} ${myBidPrice}*${myBidAmount}</div>`);
					});
				}
			}
			function cancelAsk(type,is_ice,calback){ //取消最低卖单
				let [myAskPrice, myAskAmount, tid] = [9999, 0, 0];
				for(let i = 0; i < Asks.length; i++) {
					if(Asks[i].Price < myAskPrice && Asks[i].Type==is_ice) [myAskPrice,myAskAmount,tid] = [Asks[i].Price, Asks[i].Amount, Asks[i].Id];
				}
				if(tid){
					postData(`chara/ask/cancel/${tid}`, null).then((d)=>{
						console.log(`${time} ${type}: 取消卖单#${charaId} ${myAskPrice}*${myAskAmount}`);
						$('.info_box .result').prepend(`<div class="row">${time} ${type}: 取消卖单 #${charaId} ${charas[i].name} ${myAskPrice}*${myAskAmount}</div>`);
                        calback();
					});
				}
                else calback();
			}
			function postBid(price, amount, type, is_ice){
				let args = is_ice ? '/true' : '';
				postData(`chara/bid/${charaId}/${price}/${amount}${args}`, null).then((d)=>{
					if(d.Message){
						console.log(`${time} ${type}: ${charaId} ${d.Message}`);
						$('.info_box .result').prepend(`<div class="row">${time} ${type}: #${charaId} ${charas[i].name} ${d.Message}</div>`);
					}
					else{
						console.log(`${time} ${type}: 买入委托#${charaId} ${price}*${amount}`);
						$('.info_box .result').prepend(`<div class="row">${time} ${type}: 买入委托 #${charaId} ${charas[i].name} ${price}*${amount}</div>`);
					}
				});
			}
			function postAsk(price, amount, type,is_ice){
				let args = is_ice ? '/true' : '';
				postData(`chara/ask/${charaId}/${price}/${amount}${args}`, null).then((d)=>{
					if(d.Message){
						console.log(`${time} ${type}: ${charaId} ${d.Message}`);
						$('.info_box .result').prepend(`<div class="row">${time} ${type}: #${charaId} ${charas[i].name} ${d.Message}</div>`);
					}
					else{
						console.log(`${time} ${type}: 卖出委托#${charaId} ${price}*${amount}`);
						$('.info_box .result').prepend(`<div class="row">${time} ${type}: 卖出委托 #${charaId} ${charas[i].name} ${price}*${amount}</div>`);
					}
				});
			}
			function getOverBids(Bids){
				let overBidPrice = 0;
				let count = 0, limit = 10;
				for(let i = 0; i < Bids.length; i++) {
					if(Bids[i].Price == formatBidPrice(myBidPrice)) Bids[i].Amount -= myBidAmount;
					count += Bids[i].Amount;
					if(count >= limit){
						overBidPrice = Bids[i].Price;
						break;
					}
				}
				return overBidPrice;
			}
			function getAskin(Asks, low_price){ //获取可买入的卖单价格和总数
				let [price, amount] = [0,0];
				for(let i = 0; i < Asks.length; i++) {
					if(Asks[i].Price > 0 && Asks[i].Price <= low_price){
                        if(Asks[i].Price == formatAskPrice(myAskPrice)) Asks[i].Amount -= myAskAmount;
						amount += Asks[i].Amount;
						if(Asks[i].Amount) price = Asks[i].Price;
					}
				}
				return [price, amount];
			}

			getData(`chara/depth/${charaId}`).then((d)=>{
				let [ask_price,ask_amount] = getAskin(d.Value.Asks,charas[i].highBidPrice);
				let overBidPrice = getOverBids(d.Value.Bids);
				if(ask_price && ask_price <= charas[i].highBidPrice){ //最低卖单低于买入上限,买入
					postBid(ask_price, ask_amount, '买入', 0);
				}
				if(overBidPrice > charas[i].highBidPrice){
					cancelBid('当前买单过高',0);
				}
				else{
                    let price = Math.max(Math.min(formatBidPrice(overBidPrice)+0.5,charas[i].highBidPrice),charas[i].lowBidPrice);
					if(formatBidPrice(price) != formatBidPrice(myBidPrice)){
						cancelBid('修改买单',0);
						postBid(price, charas[i].amount, '修改买单', 0);
					}else if(myBidAmount < charas[i].amount * 0.5){
						cancelBid('补充买单',0);
						postBid(price, charas[i].amount, '补充买单', 0);
					}
				}
			});
			//若有拍卖且价格低于最高买入价,关注拍卖
            if(!followList.auctions.includes(charaId)){
                getData(`chara/user/${charaId}/tinygrail/false`).then((d)=>{
                    if (d.State == 0 && d.Value.Amount > 0 && d.Value.Price <= charas[i].highBidPrice) {
                        $('.info_box .result').prepend(`<div class="row">${time} 关注竞拍 #${charaId} ${charas[i].name}</div>`);
                        followList.auctions.unshift(charaId.toString());
                        localStorage.setItem('TinyGrail_followList',JSON.stringify(followList));
                    }
                });
            }
			if(i == charas.length-1){
				$('.info_box .title').text(`自动挂单检测完毕! ${i+1} / ${charas.length}`);
			}
		});
	}
}

function closeDialog() {
	$('#TB_overlay').remove();
	$('#TB_window').remove();
}

function cancelOrder(charaId,index){
	bidList.splice(index,1);
	localStorage.setItem('TinyGrail_bidList',JSON.stringify(bidList));

	getData(`chara/user/${charaId}`).then((d)=>{
		let Bids = d.Value.Bids;
		let tid = Bids[0] ? Bids[0].Id : null;
		if(tid){
			postData(`chara/bid/cancel/${tid}`, null).then((d)=>{
				console.log(`取消买单#${charaId} ${myBidPrice}*${myBidAmount}`);
				location.reload();
			});
		}
		location.reload();
	});
}

function openOrderDialog(chara){
	let lowBidPrice = 10, highBidPrice = 10, amount = 200;
	let inorder = false, index = 0;
	for(let i = 0; i < bidList.length; i++){
		if(bidList[i].charaId == chara.Id){
			lowBidPrice = bidList[i].lowBidPrice;
			highBidPrice = bidList[i].highBidPrice;
			amount = bidList[i].amount;
			inorder = true;
			index = i;
		}
	}
	let dialog = `<div id="TB_overlay" class="TB_overlayBG TB_overlayActive"></div>
<div id="TB_window" class="dialog" style="display:block;max-width:640px;min-width:400px;">
<div class="title" title="最低买价 / 最高买价 / 挂单数量">
自动挂单 - #${chara.Id} 「${chara.Name}」 ₵${lowBidPrice} / ${highBidPrice} / ${amount}</div>
<div class="desc">输入最低买价 / 最高买价 / 挂单数量</div>
<div class="label"><div class="trade order">
<input class="lowBidPrice" type="number" style="width:75px" title="最低买价" value="${lowBidPrice}">
<input class="highBidPrice" type="number" style="width:75px" title="最高买价" value="${highBidPrice}">
<input class="amount" type="number" style="width:75px" title="挂单数量" value="${amount}">
<button id="startOrderButton" class="active">自动挂单</button><button id="cancelOrderButton">取消挂单</button>
</div></div>
<div class="loading" style="display:none"></div>
<a id="TB_closeWindowButton" title="Close">X关闭</a>
</div>`;
	$('body').append(dialog);

	$('#TB_closeWindowButton').on('click', closeDialog);

	$('#cancelOrderButton').on('click', function(){
		if(inorder){
			alert(`取消自动挂单${chara.Name}`);
			cancelOrder(chara.Id,index);
		}
		closeDialog();
	});

	$('#startOrderButton').on('click', function () {
		let info = {};
		info.charaId = chara.Id.toString();
		info.name = chara.Name;
		info.lowBidPrice = $('.trade.order .lowBidPrice').val();
		info.highBidPrice =  $('.trade.order .highBidPrice').val();
		info.amount = $('.trade.order .amount').val();
		console.log(info);
		if(inorder){
			bidList.splice(index,1);
			bidList.unshift(info);
		}
		else bidList.unshift(info);
		console.log(bidList);
		localStorage.setItem('TinyGrail_bidList',JSON.stringify(bidList));
		alert(`启动自动挂单#${chara.Id} ${chara.Name}`);
		closeDialog();
		autoBid([info]);
	});
}

function setOrder(charaId){
	let charas = [];
	for(let i = 0; i < bidList.length; i++){
		charas.push(bidList[i].charaId);
	}
	let button;
	if(charas.includes(charaId)){
		button = `<button id="autoBidButton" class="text_button">[挂买单中]</button>`;
	}
	else{
		button = `<button id="autoBidButton" class="text_button">[挂买单]</button>`;
	}
	$('#kChartButton').before(button);

	$('#autoBidButton').on('click', () => {
		getData(`chara/${charaId}`).then((d)=>{
			let chara = d.Value;
			openOrderDialog(chara);
		});
	});
}

let times = 1;
let check_bidList = setInterval(function (){
    bidList = JSON.parse(localStorage.getItem('TinyGrail_bidList')) || [];
	if(bidList.length){
		console.log('第'+times+'次挂单检测');
		autoBid(bidList);
		times++;
	}
},35*60*1000);

function observeChara(mutationList) {
	if(!$('#grailBox .progress_bar, #grailBox .title').length) {
		fetched = false;
		return;
	}
	if(fetched) return;
	if($('#grailBox .progress_bar').length) {
		observer.disconnect();
	}// use '.progress_bar' to detect (and skip) ICO characters
	else if($('#grailBox .title').length) {
		fetched = true;
		let charaId = $('#grailBox .title .name a')[0].href.split('/').pop();
		setOrder(charaId);
	}
}
let fetched = false;
let parentNode=null, observer;
if(location.pathname.startsWith('/rakuen/topic/crt')) {
	parentNode = document.getElementById('subject_info');
	observer = new MutationObserver(observeChara);
} else if(location.pathname.startsWith('/character')) {
	parentNode = document.getElementById('columnCrtB')
	observer = new MutationObserver(observeChara);
}
if(parentNode) observer.observe(parentNode, {'childList': true, 'subtree': true});