// ==UserScript==
// @name Xueqiu Follow Helper
// @namespace https://github.com/henix/userjs/xueqiu_helper
// @description 在雪球组合上显示最近一个交易日调仓的成交价。允许为每个组合设置预算,并根据预算计算应买卖的股数。
// @author henix
// @version 20151022.1
// @include http://xueqiu.com/P/*
// @license MIT License
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_xmlhttpRequest
// @grant unsafeWindow
// @grant GM_addStyle
// ==/UserScript==
/**
* https://github.com/jed/domo/blob/master/lib/domo.js
*/
// domo.js 0.5.7
// (c) 2012 Jed Schmidt
// domo.js is distributed under the MIT license.
// For more details, see http://domo-js.com
!function() {
// Determine the global object.
var global = Function("return this")()
// Valid HTML5 tag names used to generate DOM functions.
var tags = [
"A", "ABBR", "ACRONYM", "ADDRESS", "AREA", "ARTICLE", "ASIDE", "AUDIO",
"B", "BDI", "BDO", "BIG", "BLOCKQUOTE", "BODY", "BR", "BUTTON",
"CANVAS", "CAPTION", "CITE", "CODE", "COL", "COLGROUP", "COMMAND",
"DATALIST", "DD", "DEL", "DETAILS", "DFN", "DIV", "DL", "DT", "EM",
"EMBED", "FIELDSET", "FIGCAPTION", "FIGURE", "FOOTER", "FORM", "FRAME",
"FRAMESET", "H1", "H2", "H3", "H4", "H5", "H6", "HEAD", "HEADER",
"HGROUP", "HR", "HTML", "I", "IFRAME", "IMG", "INPUT", "INS", "KBD",
"KEYGEN", "LABEL", "LEGEND", "LI", "LINK", "MAP", "MARK", "META",
"METER", "NAV", "NOSCRIPT", "OBJECT", "OL", "OPTGROUP", "OPTION",
"OUTPUT", "P", "PARAM", "PRE", "PROGRESS", "Q", "RP", "RT", "RUBY",
"SAMP", "SCRIPT", "SECTION", "SELECT", "SMALL", "SOURCE", "SPAN",
"SPLIT", "STRONG", "STYLE", "SUB", "SUMMARY", "SUP", "TABLE", "TBODY",
"TD", "TEXTAREA", "TFOOT", "TH", "THEAD", "TIME", "TITLE", "TR",
"TRACK", "TT", "UL", "VAR", "VIDEO", "WBR"
]
// Turn a camelCase string into a hyphenated one.
// Used for CSS property names and DOM element attributes.
function hyphenify(text) {
return text.replace(/[A-Z]/g, "-$&").toLowerCase()
}
// Cache select Array/Object methods
var shift = Array.prototype.shift
var unshift = Array.prototype.unshift
var concat = Array.prototype.concat
var has = Object.prototype.hasOwnProperty
// Export the Domo constructor for a CommonJS environment,
// or create a new Domo namespace otherwise.
typeof module == "object"
? module.exports = Domo
: new Domo(global.document).global(true)
// Create a new domo namespace, scoped to the given document.
function Domo(document) {
if (!document) throw new Error("No document provided.")
this.domo = this
// Create a DOM comment
this.COMMENT = function(nodeValue) {
return document.createComment(nodeValue)
}
// Create a DOM text node
this.TEXT = function(nodeValue) {
return document.createTextNode(nodeValue)
}
// Create a DOM fragment
this.FRAGMENT = function() {
var fragment = document.createDocumentFragment()
var childNodes = concat.apply([], arguments)
var length = childNodes.length
var i = 0
var child
while (i < length) {
child = childNodes[i++]
while (typeof child == "function") child = child()
if (child == null) child = this.COMMENT(child)
else if (!child.nodeType) child = this.TEXT(child)
fragment.appendChild(child)
}
return fragment
}
// Create a DOM element
this.ELEMENT = function() {
var childNodes = concat.apply([], arguments)
var nodeName = childNodes.shift()
var element = document.createElement(nodeName)
var attributes = childNodes[0]
if (attributes) {
if (typeof attributes == "object" && !attributes.nodeType) {
for (var name in attributes) if (has.call(attributes, name)) {
element.setAttribute(hyphenify(name), attributes[name])
}
childNodes.shift()
}
}
if (childNodes.length) {
element.appendChild(
this.FRAGMENT.apply(this, childNodes)
)
}
switch (nodeName) {
case "HTML":
case "HEAD":
case "BODY":
var replaced = document.getElementsByTagName(nodeName)[0]
if (replaced) replaced.parentNode.replaceChild(element, replaced)
}
return element
}
// Convenience functions to create each HTML5 element
var i = tags.length
while (i--) !function(domo, nodeName) {
domo[nodeName] =
domo[nodeName.toLowerCase()] =
function() {
unshift.call(arguments, nodeName)
return domo.ELEMENT.apply(domo, arguments)
}
}(this, tags[i])
// Create a CSS style rule
this.STYLE.on = function() {
var selector = String(shift.call(arguments))
var rules = concat.apply([], arguments)
var css = selector + "{"
var i = 0
var l = rules.length
var key
var block
while (i < l) {
block = rules[i++]
switch (typeof block) {
case "object":
for (key in block) {
css += hyphenify(key) + ":" + block[key] + ";"
}
break
case "string":
css = selector + " " + block + css
break
}
}
css += "}\n"
return css
}
// Pollute the global scope for convenience.
this.global = function(on) {
var values = this.global.values
var key
var code
if (on !== false) {
global.domo = this
for (key in this) {
code = key.charCodeAt(0)
if (code < 65 || code > 90) continue
if (this[key] == global[key]) continue
if (key in global) values[key] = global[key]
global[key] = this[key]
}
}
else {
try {
delete global.domo
} catch (e) {
global.domo = undefined
}
for (key in this) {
if (key in values) {
if (global[key] == this[key]) global[key] = values[key]
}
else delete global[key]
}
}
return this
}
// A place to store previous global properties
this.global.values = {}
}
}()
;
/**
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/sign
*/
Math.sign = Math.sign || function(x) {
x = +x; // convert to a number
if (x === 0 || isNaN(x)) {
return x;
}
return x > 0 ? 1 : -1;
};
domo.global(true);
var symbol = unsafeWindow.SNB.cubeInfo.symbol;
function myround(x) {
return Math.sign(x) * Math.round(Math.abs(x));
}
function FollowDetails(elem) {
this.elem = elem;
this.symbol = elem.getAttribute("symbol");
}
FollowDetails.prototype.repaint = function(data) {
var $this = this;
var rebalances = data.rebalances;
var budget = data.budget;
var cur_prices = data.cur_prices;
var now = new Date(rebalances.list[0].updated_at);
var lastday = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
var trs = rebalances.list.filter(function(o) { return o.updated_at > lastday && (o.status == "success" || o.status == "pending"); }).map(function(a) {
var utime = new Date(a.updated_at);
function pad(x) { return x > 10 ? x : "0" + x; }
return [TR(TD({colspan:4}, utime.getFullYear() + "-" + (utime.getMonth()+1) + "-" + utime.getDate() + " " + utime.getHours() + ":" + pad(utime.getMinutes()) + ":" + pad(utime.getSeconds()) + (a.status == "pending" ? "(待成交)" : "")))].concat(a.rebalancing_histories.map(function(r) {
var prev_weight = r.prev_weight_adjusted || 0;
var delta = r.target_weight - prev_weight;
var price = r.price || cur_prices[r.stock_symbol];
if (delta && !price) {
// 开盘前无价格,使用当前价格
GM_xmlhttpRequest({
method: "GET",
url: "http://xueqiu.com/stock/quotep.json?stockid=" + r.stock_id,
onload: function(resp) {
var info = JSON.parse(resp.responseText);
cur_prices[r.stock_symbol] = info[r.stock_id].current; // TODO: immutable map
$this.repaint(data);
}
});
}
return TR(
TD(A({ target: "_blank", href: "/S/" + r.stock_symbol }, r.stock_name), "(" + r.stock_symbol.replace(/^SH|^SZ/, "$&.") + ")"),
TD(prev_weight + "% → " + r.target_weight + "%"),
TD(delta ? (price ? (price + (r.price ? "" : "(当前价)")) : "正在获取") : "无"),
TD(delta ? (price ? myround(budget * delta / 100 / price) : "正在获取") : "无")
);
}));
}).reduce(function(a, b) { return a.concat(b); }, []);
var input = INPUT({ value: budget });
var saveBut = INPUT({ type: "button", value: "保存" });
saveBut.addEventListener("click", function() {
GM_setValue("budget." + symbol, input.value);
data.budget = parseInt(input.value, 10); // TODO: immutable map
$this.repaint(data);
});
var settings = DIV({ "class": "budget-setting" }, "预算 ", input, " 元 ", saveBut);
var output = [
TABLE.apply(null, [TR(TH("名称"), TH("百分比"), TH("参考成交价"), TH("买卖股数"))].concat(trs)),
settings
];
var elem = this.elem;
// Remove all children https://stackoverflow.com/a/3955238/1305074
while (elem.firstChild) {
elem.removeChild(elem.firstChild);
}
output.forEach(function(e) { elem.appendChild(e); });
}
GM_xmlhttpRequest({
method: "GET",
url: "http://xueqiu.com/cubes/rebalancing/history.json?cube_symbol=" + symbol + "&count=20&page=1",
onload: function(resp) {
var histories = JSON.parse(resp.responseText);
var weightCircle = document.getElementById("weight-circle");
var div = DIV({ "class": "-FollowDetails", "symbol": symbol });
weightCircle.parentNode.insertBefore(div, weightCircle);
var followDetails = new FollowDetails(div);
followDetails.repaint({
rebalances: histories,
budget: parseInt(GM_getValue("budget." + symbol, 10000), 10),
cur_prices: {}
});
}
});
GM_addStyle(
".-FollowDetails table { width: 100%; margin: 10px auto; }" +
".-FollowDetails th { font-weight: bold; }" +
".-FollowDetails th, .-FollowDetails td { border: 1px solid black; padding: 0.5em; }" +
".-FollowDetails .budget-setting { margin: 10px 0 20px 0; }"
);