Xueqiu Follow Helper

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

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==UserScript==
// @name        Xueqiu Follow Helper
// @namespace   https://github.com/henix/userjs/xueqiu_helper
// @description 在雪球组合上显示最近一个交易日调仓的成交价。允许为每个组合设置预算,并根据预算计算应买卖的股数。
// @author      henix
// @version     20200704.1
// @match       http://xueqiu.com/P/*
// @match       https://xueqiu.com/P/*
// @license     MIT License
// @require     https://cdn.jsdelivr.net/npm/[email protected]/lib/domo.js
// ==/UserScript==

/**
 * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/sign
 */
Math.sign = Math.sign || function(x) {
  return ((x > 0) - (x < 0)) || +x;
};

function insertSheet(ruleString, atstart) {
  var head = document.getElementsByTagName("head")[0];
  var style = document.createElement("style");
  var rules = document.createTextNode(ruleString);
  style.type = "text/css";
  if(style.styleSheet) {
    style.styleSheet.cssText = rules.nodeValue;
  } else {
    style.appendChild(rules);
  }
  if (atstart) {
    head.insertBefore(style, head.children[0]);
  } else {
    head.appendChild(style);
  }
}

function ajaxGetJson(url, onSuccess) {
  var xhr = new XMLHttpRequest();
  xhr.open("GET", url);
  xhr.responseType = "json";
  xhr.onload = function() {
    if (this.status == 200) {
      onSuccess(this.response);
    }
  };
  xhr.send();
}

function myround(x) {
  return Math.sign(x) * Math.round(Math.abs(x));
}

function pad2(x) {
  return x >= 10 ? x : "0" + x;
}

var symbol = location.pathname.substring("/P/".length);

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 buyfactor = data.buyfactor;
  var cur_prices = data.cur_prices;

  // 过滤掉系统分红
  rebalances.list = rebalances.list.filter(function(o) { return o.category == "user_rebalancing"; });

  var frag = document.createDocumentFragment();

  var now = new Date(rebalances.list[0].updated_at);
  var lastday = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();

  var trs = [TR(TH("名称"), TH("百分比"), TH("参考成交价" + (buyfactor != 1 ? " / 挂买价" : "")), TH("买卖股数" + (buyfactor != 1 ? " / 挂买股数" : "")))];
  for (var a of rebalances.list.filter(function(o) { return o.updated_at > lastday && (o.status == "success" || o.status == "pending"); })) {
    var utime = new Date(a.updated_at);
    trs.push(TR(TD({ colspan: 4 }, utime.getFullYear() + "-" + (utime.getMonth()+1) + "-" + utime.getDate() + " " + utime.getHours() + ":" + pad2(utime.getMinutes()) + ":" + pad2(utime.getSeconds()) + (a.status == "pending" ? "(待成交)" : ""))));
    a.rebalancing_histories.forEach(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) {
        // 开盘前无价格,使用当前价格
        ajaxGetJson("/stock/quotep.json?stockid=" + r.stock_id, function(info) {
          cur_prices[r.stock_symbol] = info[r.stock_id].current; // TODO: immutable map
          $this.repaint(data);
        });
      }
      var quantity = budget * delta / 100 / price;
      trs.push(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 ? ((buyfactor != 1 && delta > 0) ? (" / " + Math.round(price * buyfactor * 1000) / 1000) : "") : "(当前价)")) : "正在获取") : "无"),
        TD(delta ? (price ? (myround(quantity) + ((buyfactor != 1 && delta > 0) ? (" / " + Math.round(quantity / buyfactor)) : "")) : "正在获取") : "无")
      ));
    });
  }
  frag.appendChild(TABLE.apply(null, trs));

  var budgetInput = INPUT({ value: budget, size: 10 });
  var budgetSave = INPUT({ type: "button", value: "保存" });
  budgetSave.addEventListener("click", function() {
    budget = parseInt(budgetInput.value, 10);
    localStorage.setItem("follow.budget." + $this.symbol, budget);
    data.budget = budget; // TODO: immutable map
    $this.repaint(data);
  });
  var buyfactorInput = INPUT({ value: buyfactor, size: 4 });
  var buyfactorSave = INPUT({ type: "button", value: "保存" });
  buyfactorSave.addEventListener("click", function() {
    localStorage.setItem("follow.buyfactor." + $this.symbol, buyfactorInput.value);
    data.buyfactor = parseFloat(buyfactorInput.value); // TODO: immutable map
    $this.repaint(data);
  });
  frag.appendChild(DIV({ "class": "budget-setting" }, "预算 ", budgetInput, " 元 ", budgetSave, " 挂买价 = 参考成交价 * ", buyfactorInput, " ", buyfactorSave));

  this.elem.innerHTML = "";
  this.elem.appendChild(frag);
};

ajaxGetJson("/cubes/rebalancing/history.json?cube_symbol=" + symbol + "&count=20&page=1", function(histories) {
  var cubeAction = document.getElementById("cube-action");
  var div = DIV({ "class": "-FollowDetails", "symbol": symbol });
  cubeAction.parentNode.insertBefore(div, cubeAction.nextSibling);

  var followDetails = new FollowDetails(div);
  followDetails.repaint({
    rebalances: histories,
    budget: parseInt(localStorage.getItem("follow.budget." + symbol) || 10000, 10),
    buyfactor: parseFloat(localStorage.getItem("follow.buyfactor." + symbol) || 1),
    cur_prices: {}
  });
});

insertSheet(
".-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; }"
);