Xueqiu Follow Helper

在雪球组合上显示最近一个交易日调仓的成交价。允许为每个组合设置预算,并根据预算计算应买卖的股数。

目前為 2015-10-26 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name Xueqiu Follow Helper
  3. // @namespace https://github.com/henix/userjs/xueqiu_helper
  4. // @description 在雪球组合上显示最近一个交易日调仓的成交价。允许为每个组合设置预算,并根据预算计算应买卖的股数。
  5. // @author henix
  6. // @version 20151026.1
  7. // @include http://xueqiu.com/P/*
  8. // @license MIT License
  9. // @grant GM_getValue
  10. // @grant GM_setValue
  11. // @grant GM_xmlhttpRequest
  12. // @grant unsafeWindow
  13. // @grant GM_addStyle
  14. // ==/UserScript==
  15.  
  16. /**
  17. * https://github.com/jed/domo/blob/master/lib/domo.js
  18. */
  19. // domo.js 0.5.7
  20.  
  21. // (c) 2012 Jed Schmidt
  22. // domo.js is distributed under the MIT license.
  23. // For more details, see http://domo-js.com
  24.  
  25. !function() {
  26. // Determine the global object.
  27. var global = Function("return this")()
  28.  
  29. // Valid HTML5 tag names used to generate DOM functions.
  30. var tags = [
  31. "A", "ABBR", "ACRONYM", "ADDRESS", "AREA", "ARTICLE", "ASIDE", "AUDIO",
  32. "B", "BDI", "BDO", "BIG", "BLOCKQUOTE", "BODY", "BR", "BUTTON",
  33. "CANVAS", "CAPTION", "CITE", "CODE", "COL", "COLGROUP", "COMMAND",
  34. "DATALIST", "DD", "DEL", "DETAILS", "DFN", "DIV", "DL", "DT", "EM",
  35. "EMBED", "FIELDSET", "FIGCAPTION", "FIGURE", "FOOTER", "FORM", "FRAME",
  36. "FRAMESET", "H1", "H2", "H3", "H4", "H5", "H6", "HEAD", "HEADER",
  37. "HGROUP", "HR", "HTML", "I", "IFRAME", "IMG", "INPUT", "INS", "KBD",
  38. "KEYGEN", "LABEL", "LEGEND", "LI", "LINK", "MAP", "MARK", "META",
  39. "METER", "NAV", "NOSCRIPT", "OBJECT", "OL", "OPTGROUP", "OPTION",
  40. "OUTPUT", "P", "PARAM", "PRE", "PROGRESS", "Q", "RP", "RT", "RUBY",
  41. "SAMP", "SCRIPT", "SECTION", "SELECT", "SMALL", "SOURCE", "SPAN",
  42. "SPLIT", "STRONG", "STYLE", "SUB", "SUMMARY", "SUP", "TABLE", "TBODY",
  43. "TD", "TEXTAREA", "TFOOT", "TH", "THEAD", "TIME", "TITLE", "TR",
  44. "TRACK", "TT", "UL", "VAR", "VIDEO", "WBR"
  45. ]
  46.  
  47. // Turn a camelCase string into a hyphenated one.
  48. // Used for CSS property names and DOM element attributes.
  49. function hyphenify(text) {
  50. return text.replace(/[A-Z]/g, "-$&").toLowerCase()
  51. }
  52.  
  53. // Cache select Array/Object methods
  54. var shift = Array.prototype.shift
  55. var unshift = Array.prototype.unshift
  56. var concat = Array.prototype.concat
  57. var has = Object.prototype.hasOwnProperty
  58.  
  59. // Export the Domo constructor for a CommonJS environment,
  60. // or create a new Domo namespace otherwise.
  61. typeof module == "object"
  62. ? module.exports = Domo
  63. : new Domo(global.document).global(true)
  64.  
  65. // Create a new domo namespace, scoped to the given document.
  66. function Domo(document) {
  67. if (!document) throw new Error("No document provided.")
  68.  
  69. this.domo = this
  70.  
  71. // Create a DOM comment
  72. this.COMMENT = function(nodeValue) {
  73. return document.createComment(nodeValue)
  74. }
  75.  
  76. // Create a DOM text node
  77. this.TEXT = function(nodeValue) {
  78. return document.createTextNode(nodeValue)
  79. }
  80.  
  81. // Create a DOM fragment
  82. this.FRAGMENT = function() {
  83. var fragment = document.createDocumentFragment()
  84. var childNodes = concat.apply([], arguments)
  85. var length = childNodes.length
  86. var i = 0
  87. var child
  88.  
  89. while (i < length) {
  90. child = childNodes[i++]
  91.  
  92. while (typeof child == "function") child = child()
  93.  
  94. if (child == null) child = this.COMMENT(child)
  95.  
  96. else if (!child.nodeType) child = this.TEXT(child)
  97.  
  98. fragment.appendChild(child)
  99. }
  100.  
  101. return fragment
  102. }
  103.  
  104. // Create a DOM element
  105. this.ELEMENT = function() {
  106. var childNodes = concat.apply([], arguments)
  107. var nodeName = childNodes.shift()
  108. var element = document.createElement(nodeName)
  109. var attributes = childNodes[0]
  110.  
  111. if (attributes) {
  112. if (typeof attributes == "object" && !attributes.nodeType) {
  113. for (var name in attributes) if (has.call(attributes, name)) {
  114. element.setAttribute(hyphenify(name), attributes[name])
  115. }
  116.  
  117. childNodes.shift()
  118. }
  119. }
  120.  
  121. if (childNodes.length) {
  122. element.appendChild(
  123. this.FRAGMENT.apply(this, childNodes)
  124. )
  125. }
  126.  
  127. switch (nodeName) {
  128. case "HTML":
  129. case "HEAD":
  130. case "BODY":
  131. var replaced = document.getElementsByTagName(nodeName)[0]
  132.  
  133. if (replaced) replaced.parentNode.replaceChild(element, replaced)
  134. }
  135.  
  136. return element
  137. }
  138.  
  139. // Convenience functions to create each HTML5 element
  140. var i = tags.length
  141. while (i--) !function(domo, nodeName) {
  142. domo[nodeName] =
  143. domo[nodeName.toLowerCase()] =
  144.  
  145. function() {
  146. unshift.call(arguments, nodeName)
  147. return domo.ELEMENT.apply(domo, arguments)
  148. }
  149. }(this, tags[i])
  150.  
  151. // Create a CSS style rule
  152. this.STYLE.on = function() {
  153. var selector = String(shift.call(arguments))
  154. var rules = concat.apply([], arguments)
  155. var css = selector + "{"
  156. var i = 0
  157. var l = rules.length
  158. var key
  159. var block
  160.  
  161. while (i < l) {
  162. block = rules[i++]
  163.  
  164. switch (typeof block) {
  165. case "object":
  166. for (key in block) {
  167. css += hyphenify(key) + ":" + block[key] + ";"
  168. }
  169. break
  170.  
  171. case "string":
  172. css = selector + " " + block + css
  173. break
  174. }
  175. }
  176.  
  177. css += "}\n"
  178.  
  179. return css
  180. }
  181.  
  182. // Pollute the global scope for convenience.
  183. this.global = function(on) {
  184. var values = this.global.values
  185. var key
  186. var code
  187.  
  188. if (on !== false) {
  189. global.domo = this
  190.  
  191. for (key in this) {
  192. code = key.charCodeAt(0)
  193.  
  194. if (code < 65 || code > 90) continue
  195.  
  196. if (this[key] == global[key]) continue
  197.  
  198. if (key in global) values[key] = global[key]
  199.  
  200. global[key] = this[key]
  201. }
  202. }
  203.  
  204. else {
  205. try {
  206. delete global.domo
  207. } catch (e) {
  208. global.domo = undefined
  209. }
  210.  
  211. for (key in this) {
  212. if (key in values) {
  213. if (global[key] == this[key]) global[key] = values[key]
  214. }
  215.  
  216. else delete global[key]
  217. }
  218. }
  219.  
  220. return this
  221. }
  222.  
  223. // A place to store previous global properties
  224. this.global.values = {}
  225. }
  226. }()
  227. ;
  228.  
  229. /**
  230. * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/sign
  231. */
  232. Math.sign = Math.sign || function(x) {
  233. x = +x; // convert to a number
  234. if (x === 0 || isNaN(x)) {
  235. return x;
  236. }
  237. return x > 0 ? 1 : -1;
  238. };
  239.  
  240. domo.global(true);
  241.  
  242. var symbol = unsafeWindow.SNB.cubeInfo.symbol;
  243.  
  244. function myround(x) {
  245. return Math.sign(x) * Math.round(Math.abs(x));
  246. }
  247.  
  248. function FollowDetails(elem) {
  249. this.elem = elem;
  250. this.symbol = elem.getAttribute("symbol");
  251. }
  252.  
  253. FollowDetails.prototype.repaint = function(data) {
  254. var $this = this;
  255. var rebalances = data.rebalances;
  256. var budget = data.budget;
  257. var buyfactor = data.buyfactor;
  258. var cur_prices = data.cur_prices;
  259.  
  260. var now = new Date(rebalances.list[0].updated_at);
  261. var lastday = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
  262.  
  263. var trs = rebalances.list.filter(function(o) { return o.updated_at > lastday && (o.status == "success" || o.status == "pending"); }).map(function(a) {
  264. var utime = new Date(a.updated_at);
  265. function pad(x) { return x >= 10 ? x : "0" + x; }
  266. 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) {
  267. var prev_weight = r.prev_weight_adjusted || 0;
  268. var delta = r.target_weight - prev_weight;
  269. var price = r.price || cur_prices[r.stock_symbol];
  270. if (delta && !price) {
  271. // 开盘前无价格,使用当前价格
  272. GM_xmlhttpRequest({
  273. method: "GET",
  274. url: "http://xueqiu.com/stock/quotep.json?stockid=" + r.stock_id,
  275. onload: function(resp) {
  276. var info = JSON.parse(resp.responseText);
  277. cur_prices[r.stock_symbol] = info[r.stock_id].current; // TODO: immutable map
  278. $this.repaint(data);
  279. }
  280. });
  281. }
  282. var quantity = budget * delta / 100 / price;
  283. return TR(
  284. TD(A({ target: "_blank", href: "/S/" + r.stock_symbol }, r.stock_name), "(" + r.stock_symbol.replace(/^SH|^SZ/, "$&.") + ")"),
  285. TD(prev_weight + "% → " + r.target_weight + "%"),
  286. TD(delta ? (price ? (price + (r.price ? ((buyfactor != 1 && delta > 0) ? (" / " + Math.round(price * buyfactor * 1000) / 1000) : "") : "(当前价)")) : "正在获取") : "无"),
  287. TD(delta ? (price ? (myround(quantity) + ((buyfactor != 1 && delta > 0) ? (" / " + Math.round(quantity / buyfactor)) : "")) : "正在获取") : "无")
  288. );
  289. }));
  290. }).reduce(function(a, b) { return a.concat(b); }, []);
  291.  
  292. var budgetInput = INPUT({ value: budget, size: 10 });
  293. var budgetSave = INPUT({ type: "button", value: "保存" });
  294. budgetSave.addEventListener("click", function() {
  295. budget = parseInt(budgetInput.value, 10);
  296. GM_setValue("budget." + $this.symbol, budget);
  297. data.budget = budget; // TODO: immutable map
  298. $this.repaint(data);
  299. });
  300. var buyfactorInput = INPUT({ value: buyfactor, size: 5 });
  301. var buyfactorSave = INPUT({ type: "button", value: "保存" });
  302. buyfactorSave.addEventListener("click", function() {
  303. GM_setValue("buyfactor." + $this.symbol, buyfactorInput.value);
  304. data.buyfactor = parseFloat(buyfactorInput.value); // TODO: immutable map
  305. $this.repaint(data);
  306. });
  307. var settings = DIV({ "class": "budget-setting" }, "预算 ", budgetInput, " 元 ", budgetSave, " 挂买价 = 参考成交价 * ", buyfactorInput, " ", buyfactorSave);
  308.  
  309. var output = [
  310. TABLE.apply(null, [TR(TH("名称"), TH("百分比"), TH("参考成交价" + (buyfactor != 1 ? " / 挂买价" : "")), TH("买卖股数" + (buyfactor != 1 ? " / 挂买股数" : "")))].concat(trs)),
  311. settings
  312. ];
  313.  
  314. var elem = this.elem;
  315. // Remove all children https://stackoverflow.com/a/3955238/1305074
  316. while (elem.firstChild) {
  317. elem.removeChild(elem.firstChild);
  318. }
  319. output.forEach(function(e) { elem.appendChild(e); });
  320. }
  321.  
  322. GM_xmlhttpRequest({
  323. method: "GET",
  324. url: "http://xueqiu.com/cubes/rebalancing/history.json?cube_symbol=" + symbol + "&count=20&page=1",
  325. onload: function(resp) {
  326. var histories = JSON.parse(resp.responseText);
  327.  
  328. var weightCircle = document.getElementById("weight-circle");
  329. var div = DIV({ "class": "-FollowDetails", "symbol": symbol });
  330. weightCircle.parentNode.insertBefore(div, weightCircle);
  331.  
  332. var followDetails = new FollowDetails(div);
  333. followDetails.repaint({
  334. rebalances: histories,
  335. budget: parseInt(GM_getValue("budget." + symbol, 10000), 10),
  336. buyfactor: parseFloat(GM_getValue("buyfactor." + symbol, 1)),
  337. cur_prices: {}
  338. });
  339. }
  340. });
  341.  
  342. GM_addStyle(
  343. ".-FollowDetails table { width: 100%; margin: 10px auto; }" +
  344. ".-FollowDetails th { font-weight: bold; }" +
  345. ".-FollowDetails th, .-FollowDetails td { border: 1px solid black; padding: 0.5em; }" +
  346. ".-FollowDetails .budget-setting { margin: 10px 0 20px 0; }"
  347. );