Greasy Fork 支持简体中文。

Xueqiu Follow Helper

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

// ==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; }"
);