// ──────────────────────────────────────────────────────────── // 週次レポート(先週分・月〜日) // すべてクライアント集計: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 (
{kicker}

{title}

{children}
); } function WrEmpty({ children }) { return
{children}
; } function WrDelta({ d, isNew }) { if (isNew) return NEW; if (d === 0) return ±0; const up = d > 0; return ( {up ? "▲" : "▼"}{Math.abs(d)} ); } // ---- 本体 ---- function WeeklyReportBody({ navigate }) { const rec = window.useRecentEntries ? window.useRecentEntries(98) : { entries: [], loading: false }; const dip = window.useDipSignals ? window.useDipSignals() : null; const weeks = React.useMemo(() => _wrWeekList(rec.entries), [rec.entries]); const [wi, setWi] = React.useState(0); // 0 = 最新週 React.useEffect(() => { if (wi >= weeks.length) setWi(0); }, [weeks.length]); const sel = weeks[Math.min(wi, Math.max(0, weeks.length - 1))] || null; const B = React.useMemo(() => sel ? { lw: sel.lw, pw: sel.pw, lastMon: sel.mon, lastSun: sel.lastSun, isFallback: false, isLatest: sel.isLatest, } : null, [sel]); // テーマ潮流(先週 vs 前週) const themes = React.useMemo(() => { if (!B) return { rows: [], faded: [] }; const lwm = _wrThemeCount(B.lw), pwm = _wrThemeCount(B.pw); const rows = Object.entries(lwm) .map(([name, n]) => ({ name, n, prev: pwm[name] || 0, d: n - (pwm[name] || 0), isNew: !pwm[name] })) .sort((a, b) => b.n - a.n || b.d - a.d) .slice(0, 7); const faded = Object.entries(pwm) .filter(([name, n]) => n >= 2 && !lwm[name]) .map(([name, n]) => ({ name, prev: n })) .sort((a, b) => b.prev - a.prev) .slice(0, 3); return { rows, faded }; }, [B]); // 検知後変化率つきリスト const perf = React.useMemo(() => { if (!B) return { ranked: [], up: [], down: [] }; const withPct = B.lw .map(e => ({ e, pct: _wrPct(e) })) .filter(x => x.pct != null); const ranked = [...withPct].sort((a, b) => b.pct - a.pct); const upAll = ranked.filter(x => x.pct > 0); const downAll = [...withPct].sort((a, b) => a.pct - b.pct).filter(x => x.pct < 0); return { ranked: ranked.slice(0, 4), up: upAll.slice(0, 3), down: downAll.slice(0, 3), upAll, downAll, }; }, [B]); // 押し目(もうすぐ / 現在) const dips = React.useMemo(() => { if (!dip || dip.state.status !== "ready") return { now: [], soon: [] }; const items = (window.DipSignals && window.DipSignals.demo && dip.state.demoItems) ? dip.state.demoItems : (window.DipSignals ? window.DipSignals._watchItems() : []); const nameOf = {}; items.forEach(it => { nameOf[it.code] = it.name || it.code; }); const now = [], soon = []; Object.entries(dip.state.signals || {}).forEach(([code, s]) => { if (!s || !s.armed || s.ended) return; if (s.stage >= 1) now.push({ code, name: nameOf[code] || code, s }); else if (s.distMa25 != null && s.distMa25 <= 3) soon.push({ code, name: nameOf[code] || code, s }); }); now.sort((a, b) => b.s.stage - a.s.stage); soon.sort((a, b) => a.s.distMa25 - b.s.distMa25); return { now, soon }; }, [dip && dip.state]); if (!B) { return (
{rec.loading ? "集計データを読み込み中..." : "集計対象のデータがまだありません。"}
); } const weekNo = _wrIsoWeek(B.lastMon); const rng = `${_wrMD(B.lastMon)}(月)— ${_wrMD(B.lastSun)}(日)`; const diff = B.lw.length - B.pw.length; const topTheme = themes.rows[0] || null; const best = perf.ranked[0] || null; const W = _wrPickWords(B, themes, perf); // 巻頭コメント(アナリスト風の導入段落をデータから組み立て) const lead = [ `先週(${rng})の初動検知は${B.lw.length}件と、前週の${B.pw.length}件から${W.trend}。${W.mood}。`, W.themeLead, W.hitLine, best ? `個別では${best.e.name}(${best.e.code})が検知後+${best.pct.toFixed(1)}%と週の最高記録。以下、テーマの潮流と個別の振り返り、押し目の現況を順に整理します。` : "以下、テーマの潮流と個別の振り返りを順に整理します。", ].filter(Boolean).join(""); // 総括3行 const summary = [ <>先週の初動検知は {B.lw.length}件(前週比 0 ? "#ff8a8f" : diff < 0 ? "#7fa7d9" : "#9aa3b8" }}>{diff > 0 ? `+${diff}` : diff}件)。, topTheme ? <>資金の中心は「{topTheme.name}」({topTheme.n}件{topTheme.isNew ? "・今週浮上" : topTheme.d > 0 ? `・前週比+${topTheme.d}` : ""})。 : <>明確なテーマ集中は見られませんでした。, best ? <>検知後の最高は {best.e.code} {best.e.name} の +{best.pct.toFixed(1)}%(現在値比)。 : <>検知後の値動きデータは集計中です。, ]; return (
{/* ヘッダー */}
{B.isLatest ? "先週分 · 毎週土曜更新" : "バックナンバー"}
有料会員限定 · WEEKLY REPORT

週次レポート

第 {weekNo} 週 · {rng} · 検知 {B.lw.length}
{/* 週の切替(バックナンバー) */} {weeks.length > 1 && (
)}
{/* 1. 総括 */}

{lead}

{summary.map((s, i) => (
0{i + 1}

{s}

))}
{/* 2. テーマ潮流 */}
テーマ別の検知件数 / 前週比
{themes.rows.length === 0 && 先週はテーマ付きの検知がありませんでした。} {themes.rows.length > 0 && (

{W.themePara}

)}
{themes.rows.map((t, i) => { const max = Math.max(...themes.rows.map(x => x.n), 1); return (
{String(i + 1).padStart(2, "0")}
{t.name}
{t.n}件
); })}
{themes.faded.length > 0 && (
後退:{themes.faded.map(f => `${f.name}(前週${f.prev}件 → 0)`).join(" / ")}
)} {/* 3. 注目初動(抜粋) */} {perf.ranked.length === 0 && 検知後の値動きデータがまだありません。} {W.picksPara && (

{W.picksPara}

)}
{perf.ranked.map(({ e, pct }) => ( ))}
{/* 4. 効いた / 続かなかった */} {W.reviewPara && (

{W.reviewPara}

)}
{[ { label: "効いた", rows: perf.up, col: "#ff8a8f", empty: "プラス圏の初動なし" }, { label: "続かなかった", rows: perf.down, col: "#7fa7d9", empty: "マイナス圏の初動なし" }, ].map(g => (
{g.label}
{g.rows.length === 0 &&
{g.empty}
}
{g.rows.map(({ e, pct }) => (
{e.name} {pct >= 0 ? "+" : ""}{pct.toFixed(1)}%
))}
))}

※ 外れた初動も含めて開示しています。変化率は検知時価格 → 現在値。

{/* 5. 押し目シグナル(現時点の状況なので最新週のみ) */} {B.isLatest && (

急騰後の銘柄が最初の調整でどこまで押されるかは、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 && ( 対象なし。ウォッチ登録した急騰銘柄に押し目が来るとここに載ります。 )} {dips.now.length > 0 && (
現在押し目
{dips.now.map(x => (
{x.s.stage >= 2 ? "②" : "①"} {x.code} {x.name} {x.s.stage >= 2 ? "75日線タッチ" : "25日線タッチ"}
))}
)} {dips.soon.length > 0 && (
もうすぐ押し目(25日線接近)
{dips.soon.map(x => (
{x.code} {x.name} あと{Math.max(0, x.s.distMa25).toFixed(1)}%
))}
)}
)} {/* 6. 来週の観測ポイント */}
{[ topTheme && { h: `「${topTheme.name}」の継続性`, b: topTheme.isNew ? "今週初めて浮上したテーマ。2週目も検知が続くかで、一過性か資金の定着かを見極めます。" : `${topTheme.prev > 0 ? "2週連続" : "先週"}で最多の検知。物色が3週目に入ると過熱域に近づく事例もあり、件数の伸びと質(理由の強さ)を確認します。`, }, themes.rows.filter(t => t.isNew).length > 0 && { h: "新規テーマの2週目", b: `${themes.rows.filter(t => t.isNew).slice(0, 3).map(t => `「${t.name}」`).join("")}が初登場。翌週に追随検知が出るかが定着のサインです。`, }, dips.soon.length > 0 && B.isLatest && { h: "25日線への接触", b: `${dips.soon.slice(0, 2).map(x => `${x.name}(あと${Math.max(0, x.s.distMa25).toFixed(1)}%)`).join("、")}。接触すれば押し目①として通知されます。`, }, perf.downAll.length > perf.upAll.length && { h: "初動の質の低下", b: "先週はマイナス圏の初動が多数派でした。追いかけ買いより、押し目やテーマの2番手を待つ姿勢が合う環境です。", }, ].filter(Boolean).map((p, i) => (
{p.h}

{p.b}

))}
{/* 7. 注記 */}

集計対象:{rng} に検知した初動 {B.lw.length} 件(前週 {B.pw.length} 件)。 変化率は検知時価格から現在値までの騰落率で、閲覧時点により変動します。 {!B.isLatest && "バックナンバーの変化率も最新の株価で再計算しているため、発行当時の数値とは異なる場合があります。"} 押し目シグナルはウォッチ登録銘柄の日足移動平均(25日・75日)への接触判定です。 本レポートは情報整理を目的としたものであり、売買の推奨ではありません。投資判断はご自身の責任でお願いします。

); } Object.assign(window, { WeeklyReportBody });