Greasy Fork

Greasy Fork is available in English.

Xueqiu Follow Helper

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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