等大佬研究研究回测下,看看是不是9:40强于沪指的票更好点?

`// ==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)) returnsh${code};
if (/^[03]\d{5}$/.test(code)) returnsz${code};
if (/^[489]\d{5}$/.test(code)) returnbj${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();
})();
`技术流来了~
我来试试看看不懂,点了