// ────────────────────────────────────────────────────────────
// 週次レポート(先週分・月〜日)
// すべてクライアント集計:useRecentEntries(21) で先週+前週を取得し、
// テーマ潮流の差分 / 注目初動 / 効いた・続かなかった / 押し目状況 を組む。
// 追加バックエンドなし。pages.jsx の ReportPage から呼ばれる。
// ────────────────────────────────────────────────────────────
// JSTの日付演算は「+9hシフトしてUTCゲッターで読む」方式で統一
const _WR_DAY = 86400000;
function _wrShift(t) { return new Date(t + 9 * 3600000); }
function _wrMD(shiftedMs) {
const d = new Date(shiftedMs);
return `${d.getUTCMonth() + 1}.${d.getUTCDate()}`;
}
function _wrIsoWeek(shiftedMs) {
const t = new Date(shiftedMs);
const dayNum = (t.getUTCDay() + 6) % 7;
t.setUTCDate(t.getUTCDate() - dayNum + 3);
const firstThu = new Date(Date.UTC(t.getUTCFullYear(), 0, 4));
return 1 + Math.round(((t - firstThu) / _WR_DAY - 3 + ((firstThu.getUTCDay() + 6) % 7)) / 7);
}
function _wrPct(e) {
const sp = Number(e.surge_price ?? e.price);
const cp = e.current_price != null ? Number(e.current_price) : NaN;
if (!sp || sp <= 0 || isNaN(cp)) return null;
return ((cp - sp) / sp) * 100;
}
// 全週リスト(月起点)を新しい順に作る。完了週(先週以前)のみ。
// 検知が1件もない週はスキップ。最大12週分。
function _wrWeekList(entries) {
const list = (entries || []).filter(e => e && e.detected_at);
if (!list.length) return [];
const now = _wrShift(Date.now()).getTime();
const dow = (new Date(now).getUTCDay() + 6) % 7;
const todayMid = Math.floor(now / _WR_DAY) * _WR_DAY;
// 土日は市場が閉まっているため、土曜0時(JST)の時点で当週(月〜金)を完了週として扱う
const thisMon = todayMid - dow * _WR_DAY + (dow >= 5 ? 7 * _WR_DAY : 0); // これより前が完了週
const ms = e => _wrShift(new Date(e.detected_at).getTime()).getTime();
const byWeek = {};
list.forEach(e => {
const t = ms(e);
if (t >= thisMon) return; // 今週分はまだレポート化しない
const mon = t - ((new Date(Math.floor(t / _WR_DAY) * _WR_DAY).getUTCDay() + 6) % 7) * _WR_DAY;
const monMid = Math.floor(mon / _WR_DAY) * _WR_DAY;
(byWeek[monMid] = byWeek[monMid] || []).push(e);
});
const mons = Object.keys(byWeek).map(Number).sort((a, b) => b - a).slice(0, 12);
return mons.map(mon => ({
mon,
lw: byWeek[mon],
pw: byWeek[mon - 7 * _WR_DAY] || [],
lastSun: mon + 6 * _WR_DAY,
isLatest: mon === mons[0],
}));
}
function _wrThemeCount(pool) {
const m = {};
pool.forEach(e => (e.themes || []).forEach(t => {
const k = String(t).trim();
if (k) m[k] = (m[k] || 0) + 1;
}));
return m;
}
// 分析コメントの自動組立(データから文章を生成。推奨は書かない)
function _wrPickWords(B, themes, perf) {
const n = B.lw.length, p = B.pw.length, diff = n - p;
const trend = diff >= 3 ? "明確に増加" : diff >= 1 ? "小幅に増加" : diff === 0 ? "横ばい" : diff >= -2 ? "小幅に減少" : "明確に減少";
const mood = diff >= 3 ? "個別材料に資金が回りやすい地合いだったと言えます"
: diff <= -3 ? "新規の物色より様子見が優勢な週でした"
: "物色と様子見が拮抗する中立的な週でした";
const t0 = themes.rows[0], t1 = themes.rows[1];
let themeLead = "週内で突出したテーマはなく、物色は分散気味でした。";
if (t0) {
const t0trend = t0.isNew ? "今週新たに浮上したテーマであり、初動段階の可能性があります"
: t0.d > 0 ? `前週(${t0.prev}件)から勢いを増しており、資金の続きが意識される局面です`
: t0.d === 0 ? "前週と同ペースで、定着した物色対象になりつつあります"
: `前週(${t0.prev}件)比では劣勢だったものの首位を維持しています`;
themeLead = `資金の中心は「${t0.name}」で週内${t0.n}件。${t0trend}。`;
if (t1) themeLead += `次点は「${t1.name}」(${t1.n}件${t1.isNew ? "・新規" : t1.d > 0 ? `・前週比+${t1.d}` : ""})。`;
}
const withPct = perf.upAll.length + perf.downAll.length;
const hitLine = withPct > 0
? `検知後にプラスを維持しているのは${withPct}件中${perf.upAll.length}件。` +
(perf.upAll.length / withPct >= 0.6 ? "初動の続きが良く、追風の強い週でした。"
: perf.upAll.length / withPct >= 0.4 ? "続伸と失速が混在しており、材料の質で明暗が分かれています。"
: "初動後の失速が目立ち、短期の利食いが早い環境でした。")
: "";
// ---- 各セクションの本文段落(総括並みの分量) ----
// テーマ潮流:集中度・新規・後退まで含めた段落
let themePara = "";
if (t0) {
const themedTotal = themes.rows.reduce((a, t) => a + t.n, 0);
const share = themedTotal > 0 ? Math.round((t0.n / themedTotal) * 100) : 0;
const t2 = themes.rows[2];
themePara = themeLead;
if (t2) themePara += `3番手には「${t2.name}」(${t2.n}件)が入りました。`;
themePara += share >= 40
? `上位テーマへの集中度は高く(首位だけで約${share}%)、物色は一極集中に近い状態です。こうした局面はテーマの当たり外れがはっきり出るため、中心テーマの周辺銘柄(2番手・3番手)に検知が広がるかが次のチェックポイントになります。`
: share >= 25
? `首位のシェアは約${share}%と適度な集中にとどまり、複数テーマが並行して動く健全な分布です。特定テーマの崩れが全体に波及しにくい週だったと言えます。`
: `首位でもシェア約${share}%と物色は広く分散しており、テーマより個別材料主導の週でした。`;
const news = themes.rows.filter(t => t.isNew);
if (news.length > 0) themePara += `新規に浮上したのは${news.map(t => `「${t.name}」`).join("")}。初動の早い段階であり、今週も検知が続くかで一過性か定着かを見極めます。`;
if (themes.faded.length > 0) themePara += `一方、前週に賑わった${themes.faded.map(f => `「${f.name}」`).join("")}は今週ゼロ件と後退しました。短命に終わった可能性があり、戻りを追う際は注意が必要です。`;
}
// 注目初動:首位・次点の物語+テーマとの接続
let picksPara = "";
const b0 = perf.ranked[0], b1 = perf.ranked[1];
if (b0) {
const dys = Math.max(1, Math.round((Date.now() - new Date(b0.e.detected_at).getTime()) / _WR_DAY));
picksPara = `最も効いたのは${b0.e.name}(${b0.e.code})で、検知から${dys}日で+${b0.pct.toFixed(1)}%。`;
if (b1) picksPara += `次いで${b1.e.name}が${b1.pct >= 0 ? "+" : ""}${b1.pct.toFixed(1)}%と続きます。`;
if (t0) {
const hits = perf.ranked.filter(x => (x.e.themes || []).includes(t0.name)).length;
picksPara += hits >= 2 ? `上位には首位テーマ「${t0.name}」絡みが${hits}件入っており、テーマの勢いがそのまま個別の続伸につながっています。`
: hits === 1 ? `首位テーマ「${t0.name}」絡みは上位に1件。テーマと個別の強さが必ずしも一致していない点は留意です。`
: `なお、上位に首位テーマ「${t0.name}」絡みは入っておらず、件数の多さと値動きの強さが乖離しています。`;
}
picksPara += "検知時の理由と合わせて、何が効いたのかを下のカードで確認できます。";
}
// 効いた/続かなかった:勝率+平均+両端
let reviewPara = hitLine;
if (withPct > 0) {
const avg = arr => arr.length ? arr.reduce((a, x) => a + x.pct, 0) / arr.length : null;
const avgUp = avg(perf.upAll), avgDn = avg(perf.downAll);
if (avgUp != null) reviewPara += `プラス圏の平均は+${avgUp.toFixed(1)}%`;
if (avgDn != null) reviewPara += `${avgUp != null ? "、" : ""}マイナス圏の平均は${avgDn.toFixed(1)}%。`;
else if (avgUp != null) reviewPara += "。";
const worst = perf.downAll[0];
if (worst) reviewPara += `最も崩れたのは${worst.e.name}(${worst.pct.toFixed(1)}%)で、初動の翌日以降に続きが入らなかったパターンです。`;
reviewPara += "当たったものだけでなく外れたものも含めて毎週開示し、検知ロジックの精度をそのまま見ていただく方針です。";
}
return { trend, mood, themeLead, hitLine, themePara, picksPara, reviewPara };
}
// ---- 小物 ----
function WrSection({ title, kicker, children }) {
return (
{title}
{children}
{lead}
{s}
{W.themePara}
)}{W.picksPara}
)}{W.reviewPara}
)}※ 外れた初動も含めて開示しています。変化率は検知時価格 → 現在値。
急騰後の銘柄が最初の調整でどこまで押されるかは、25日線・75日線が目安になります。 当サイトではウォッチ登録銘柄の日足を毎日判定し、25日線タッチを押し目①、75日線タッチを押し目②として記録、 終値で75日線を割り込んだ場合は「調整入り」として自動的に追跡を終了します。 {dips.now.length > 0 && `現在、ウォッチ中の${dips.now.length}銘柄が押し目ゾーンに到達しています。${dips.now.slice(0, 2).map(x => `${x.name}は${x.s.stage >= 2 ? "75日線まで到達した深めの調整" : "25日線タッチの浅い押し目"}`).join("、")}という位置関係です。`} {dips.soon.length > 0 && `${dips.soon.map(x => x.name).slice(0, 2).join("と")}は25日線まであとわずかで、今週の接触が視野に入ります。接触すれば通知パネルに①として表示されます。`} {(dips.now.length + dips.soon.length) === 0 && "現在、押し目ゾーンにあるウォッチ銘柄はありません。急騰直後の銘柄は高値掴みになりやすく、最初の押し目を待ってから検討する方針の方にこの欄を活用いただけます。"}
{(dips.now.length + dips.soon.length) === 0 && ({p.b}
集計対象:{rng} に検知した初動 {B.lw.length} 件(前週 {B.pw.length} 件)。 変化率は検知時価格から現在値までの騰落率で、閲覧時点により変動します。 {!B.isLatest && "バックナンバーの変化率も最新の株価で再計算しているため、発行当時の数値とは異なる場合があります。"} 押し目シグナルはウォッチ登録銘柄の日足移動平均(25日・75日)への接触判定です。 本レポートは情報整理を目的としたものであり、売買の推奨ではありません。投資判断はご自身の責任でお願いします。