待大佬研究:比沪指强的更强点?

492
  • ostaer_summar

    等大佬研究研究回测下,看看是不是9:40强于沪指的票更好点?
    paste-20260910-100925.png
    `// ==UserScript==
    // @name 古诗策略相对强度标记
    // @namespace https://i.gushi.in/
    // @version 0.1.0
    // @description 按页面交易日标记09:40强于上证指数的策略股票。
    // @author guishi
    // @match https://i.gushi.in/factor.html*
    // @run-at document-idle
    // @grant GM_xmlhttpRequest
    // @connect quotes.sina.cn
    // ==/UserScript==

    (function () {
    'use strict';

    const INDEX_SYMBOL = 'sh000001';
    const STYLE_ID = 'gushi-relative-strength-style';
    const SUMMARY_ID = 'gushi-relative-strength-summary';
    const barRequests = new Map();
    let analysisEpoch = 0;
    let analyzeTimer = 0;
    let lastSignature = '';
    let onlyStrong = false;

    function parsePageContext(href) {
    const url = new URL(href);
    const tradeDate = String(url.searchParams.get('date') || '').trim();
    return {
    tradeDate: /^\d{4}-\d{2}-\d{2}$/.test(tradeDate) ? tradeDate : '',
    factorId: String(url.searchParams.get('factor') || '').trim(),
    };
    }

    function inferSinaSymbol(stockCode) {
    const code = String(stockCode || '').trim().slice(0, 6);
    if (/^6\d{5}$/.test(code)) return sh${code};
    if (/^[03]\d{5}$/.test(code)) return sz${code};
    if (/^[489]\d{5}$/.test(code)) return bj${code};
    return '';
    }

    function calculateRelativeStrength(stockBars, indexBars, tradeDate) {
    const stockPrices = pricesForRelativeStrength(stockBars, tradeDate);
    const indexPrices = pricesForRelativeStrength(indexBars, tradeDate);
    if (!stockPrices || !indexPrices) {
    return { available: false, reason: '09:40数据缺失' };
    }

    const stockChangePct = (stockPrices.target / stockPrices.previousClose - 1) * 100;
    const indexChangePct = (indexPrices.target / indexPrices.previousClose - 1) * 100;
    const relativeStrengthPct = stockChangePct - indexChangePct;
    return {
    available: true,
    stockChangePct,
    indexChangePct,
    relativeStrengthPct,
    stronger: relativeStrengthPct >= 0,
    };
    }

    function pricesForRelativeStrength(rawBars, tradeDate) {
    const bars = (Array.isArray(rawBars) ? rawBars : [])
    .map((bar) => ({ day: String(bar?.day || ''), close: Number(bar?.close) }))
    .filter((bar) => bar.day && Number.isFinite(bar.close) && bar.close > 0)
    .sort((left, right) => left.day.localeCompare(right.day));
    const previous = bars.filter((bar) => bar.day.slice(0, 10) < tradeDate).at(-1);
    const target = bars.find((bar) => bar.day === ${tradeDate} 09:40:00);
    if (!previous || !target) return null;
    return { previousClose: previous.close, target: target.close };
    }

    function formatSignedPercent(value) {
    const number = Number(value);
    if (!Number.isFinite(number)) return '-';
    return ${number >= 0 ? '+' : ''}${number.toFixed(2)}%;
    }

    function buildSinaKlineURL(symbol) {
    const url = new URL('https://quotes.sina.cn/cn/api/json_v2.php/CN_MarketDataService.getKLineData');
    url.searchParams.set('symbol', symbol);
    url.searchParams.set('scale', '5');
    url.searchParams.set('ma', 'no');
    url.searchParams.set('datalen', '1023');
    return url.toString();
    }

    function buildBadgePresentation(result) {
    if (!result?.available) {
    return { state: 'missing', text: '09:40数据缺失', title: result?.reason || '所选日期没有可用分钟线' };
    }
    const state = result.stronger ? 'strong' : 'weak';
    return {
    state,
    text: ${result.stronger ? '强于' : '弱于'}沪指 ${formatSignedPercent(result.relativeStrengthPct)},
    title: 个股09:40 ${formatSignedPercent(result.stockChangePct)}|上证09:40 ${formatSignedPercent(result.indexChangePct)}|相对 ${formatSignedPercent(result.relativeStrengthPct)},
    };
    }

    function mountBadge(card, result) {
    const score = card?.querySelector?.('.stock-score');
    if (!score) return null;
    const presentation = buildBadgePresentation(result);
    let badge = card.querySelector('.gushi-rs-badge');
    if (!badge) {
    badge = document.createElement('span');
    score.after(badge);
    }
    badge.className = gushi-rs-badge gushi-rs-badge--${presentation.state};
    badge.textContent = presentation.text;
    badge.title = presentation.title;
    card.dataset.gushiRelativeState = presentation.state;
    card.classList.toggle('gushi-rs-card--strong', presentation.state === 'strong');
    card.classList.toggle('gushi-rs-card--weak', presentation.state === 'weak');
    card.classList.toggle('gushi-rs-card--missing', presentation.state === 'missing');
    return badge;
    }

    function buildSummaryText(tradeDate, results) {
    const available = (Array.isArray(results) ? results : []).filter((item) => item?.available);
    const strongCount = available.filter((item) => item.stronger).length;
    const missingCount = Math.max(0, (Array.isArray(results) ? results.length : 0) - available.length);
    const indexChangePct = available[0]?.indexChangePct;
    const parts = [
    tradeDate,
    上证09:40 ${formatSignedPercent(indexChangePct)},
    强于沪指 ${strongCount}/${available.length},
    ];
    if (missingCount > 0) parts.push(缺失 ${missingCount});
    return parts.join('|');
    }

    function loadSinaKlines(symbol) {
    const cached = barRequests.get(symbol);
    if (cached) return cached;

    const request = new Promise((resolve, reject) => {
    if (typeof GM_xmlhttpRequest !== 'function') {
    reject(new Error('油猴跨域接口不可用'));
    return;
    }
    GM_xmlhttpRequest({
    method: 'GET',
    url: buildSinaKlineURL(symbol),
    timeout: 15000,
    onload(response) {
    try {
    if (response.status < 200 || response.status >= 300) {
    throw new Error(行情接口返回 ${response.status});
    }
    const bars = JSON.parse(response.responseText || '[]');
    if (!Array.isArray(bars)) throw new Error('行情格式不正确');
    resolve(bars);
    } catch (error) {
    reject(error);
    }
    },
    ontimeout() { reject(new Error('行情请求超时')); },
    onerror() { reject(new Error('行情请求失败')); },
    });
    }).catch((error) => {
    // 失败请求不能长期留在缓存中,后续页面刷新或重新排序时允许自动重试。
    barRequests.delete(symbol);
    throw error;
    });
    barRequests.set(symbol, request);
    return request;
    }

    function resolvePageTradeDate() {
    const fromURL = parsePageContext(window.location.href).tradeDate;
    if (fromURL) return fromURL;
    const fromInput = String(document.querySelector('#dateInput')?.value || '').trim();
    return /^\d{4}-\d{2}-\d{2}$/.test(fromInput) ? fromInput : '';
    }

    function collectStockCards() {
    return [...document.querySelectorAll('#stockList .stock-card')]
    .map((card) => ({
    card,
    code: String(card.querySelector('.stock-card__code')?.textContent || '').trim().slice(0, 6),
    }))
    .filter((item) => /^\d{6}$/.test(item.code));
    }

    function mountSummary() {
    let summary = document.querySelector(#${SUMMARY_ID});
    if (summary) return summary;
    const stockList = document.querySelector('#stockList');
    if (!stockList?.parentElement) return null;

    summary = document.createElement('section');
    summary.id = SUMMARY_ID;
    summary.className = 'gushi-rs-summary';
    summary.setAttribute('aria-live', 'polite');
    summary.innerHTML = `
    <div class="gushi-rs-summary__copy">
    <strong>09:40 相对强度</strong>
    <span data-gushi-rs-summary-text>等待股票列表…</span>
    </div>
    <button type="button" data-gushi-rs-filter aria-pressed="false">只看强于沪指</button>
    `;
    summary.querySelector('[data-gushi-rs-filter]')?.addEventListener('click', (event) => {
    onlyStrong = !onlyStrong;
    event.currentTarget.setAttribute('aria-pressed', String(onlyStrong));
    event.currentTarget.textContent = onlyStrong ? '显示全部股票' : '只看强于沪指';
    applyStrongFilter();
    });
    stockList.before(summary);
    return summary;
    }

    function setSummaryText(text, state = '') {
    const summary = mountSummary();
    const target = summary?.querySelector('[data-gushi-rs-summary-text]');
    if (!target) return;
    target.textContent = text;
    target.dataset.state = state;
    }

    function applyStrongFilter() {
    document.documentElement.classList.toggle('gushi-rs-only-strong', onlyStrong);
    }

    function installStyles() {
    if (document.querySelector(#${STYLE_ID})) return;
    const style = document.createElement('style');
    style.id = STYLE_ID;
    style.textContent = `
    .gushi-rs-badge {
    display: inline-flex;
    align-items: center;
    min-height: 22px;
    margin-left: 8px;
    padding: 1px 7px;
    border: 1px solid transparent;
    border-radius: 6px;
    font-size: 12px;
    font-weight: 700;
    line-height: 1.35;
    vertical-align: middle;
    white-space: nowrap;
    }
    .gushi-rs-badge--strong {
    color: #b91c1c;
    border-color: rgba(220, 38, 38, .28);
    background: rgba(254, 226, 226, .72);
    }
    .gushi-rs-badge--weak {
    color: #475569;
    border-color: rgba(148, 163, 184, .35);
    background: rgba(241, 245, 249, .88);
    }
    .gushi-rs-badge--missing {
    color: #64748b;
    border-color: rgba(148, 163, 184, .25);
    background: transparent;
    font-weight: 500;
    }
    .stock-card.gushi-rs-card--strong {
    box-shadow: inset 3px 0 0 #dc2626;
    }
    .gushi-rs-summary {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    min-height: 58px;
    margin: 0 0 14px;
    padding: 9px 12px;
    color: #0f172a;
    border: 1px solid #dbe2ea;
    border-radius: 10px;
    background: #fff;
    box-shadow: 0 4px 14px rgba(15, 23, 42, .05);
    }
    .gushi-rs-summary__copy {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 6px 12px;
    min-width: 0;
    }
    .gushi-rs-summary__copy strong { font-size: 14px; }
    .gushi-rs-summary__copy span { color: #475569; font-size: 13px; }
    .gushi-rs-summary__copy span[data-state="error"] { color: #b91c1c; }
    .gushi-rs-summary button {
    flex: 0 0 auto;
    min-height: 44px;
    padding: 0 14px;
    color: #334155;
    border: 1px solid #cbd5e1;
    border-radius: 8px;
    background: #fff;
    font: inherit;
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
    transition: color .18s ease, border-color .18s ease, background-color .18s ease;
    }
    .gushi-rs-summary button:hover,
    .gushi-rs-summary button:focus-visible,
    .gushi-rs-summary button[aria-pressed="true"] {
    color: #b91c1c;
    border-color: #dc2626;
    background: #fef2f2;
    outline: none;
    }
    .gushi-rs-summary button:focus-visible { box-shadow: 0 0 0 3px rgba(220, 38, 38, .16); }
    .gushi-rs-only-strong #stockList .stock-card:not([data-gushi-relative-state="strong"]) { display: none !important; }
    @media (max-width: 640px) {
    .gushi-rs-summary { align-items: stretch; flex-direction: column; }
    .gushi-rs-summary button { width: 100%; }
    }
    @media (prefers-reduced-motion: reduce) {
    .gushi-rs-summary button { transition: none; }
    }
    html[data-theme="dark"] .gushi-rs-summary,
    html.dark .gushi-rs-summary {
    color: #e2e8f0;
    border-color: #334155;
    background: #111827;
    box-shadow: none;
    }
    html[data-theme="dark"] .gushi-rs-summary__copy span,
    html.dark .gushi-rs-summary__copy span { color: #cbd5e1; }
    html[data-theme="dark"] .gushi-rs-summary button,
    html.dark .gushi-rs-summary button { color: #e2e8f0; border-color: #475569; background: #1f2937; }
    html[data-theme="dark"] .gushi-rs-badge--weak,
    html.dark .gushi-rs-badge--weak { color: #cbd5e1; border-color: #475569; background: #1f2937; }
    `;
    document.head.appendChild(style);
    }

    async function analyzePage() {
    const tradeDate = resolvePageTradeDate();
    const stocks = collectStockCards();
    if (!tradeDate || stocks.length === 0) return;

    const signature = ${tradeDate}|${stocks.map((item) => item.code).join(',')};
    if (signature === lastSignature && stocks.every((item) => item.card.querySelector('.gushi-rs-badge'))) {
    applyStrongFilter();
    return;
    }
    lastSignature = signature;
    const epoch = ++analysisEpoch;
    setSummaryText(${tradeDate}|正在读取对应交易日09:40行情…);

    try {
    const indexBars = await loadSinaKlines(INDEX_SYMBOL);
    const results = await Promise.all(stocks.map(async ({ code }) => {
    const symbol = inferSinaSymbol(code);
    if (!symbol) return { available: false, reason: '股票代码无法识别' };
    try {
    return calculateRelativeStrength(await loadSinaKlines(symbol), indexBars, tradeDate);
    } catch (error) {
    return { available: false, reason: error?.message || '行情请求失败' };
    }
    }));
    if (epoch !== analysisEpoch) return;
    stocks.forEach((item, index) => mountBadge(item.card, results[index]));
    setSummaryText(buildSummaryText(tradeDate, results));
    applyStrongFilter();
    } catch (error) {
    if (epoch !== analysisEpoch) return;
    const message = error?.message || '行情请求失败';
    stocks.forEach((item) => mountBadge(item.card, { available: false, reason: message }));
    setSummaryText(${tradeDate}|相对强度加载失败:${message}, 'error');
    }
    }

    function scheduleAnalyze() {
    window.clearTimeout(analyzeTimer);
    analyzeTimer = window.setTimeout(() => { void analyzePage(); }, 80);
    }

    function boot() {
    installStyles();
    scheduleAnalyze();
    const observer = new MutationObserver(() => scheduleAnalyze());
    observer.observe(document.body, { childList: true, subtree: true });
    window.addEventListener('popstate', scheduleAnalyze);
    }

    boot();
    })();
    `

  • niulai
    #1

    技术流来了~
    我来试试看

  • Enoch
    #2

    看不懂,点了

发表回复

登录后回复