File size: 3,809 Bytes
20fef51
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42fcd29
 
 
 
 
 
 
 
 
20fef51
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
import React from 'react'
import {
  Bar,
  CartesianGrid,
  Cell,
  Legend,
  ResponsiveContainer,
  Tooltip,
  XAxis,
  YAxis,
  ComposedChart,
  Line,
  ReferenceLine,
} from 'recharts'
import { OHLCVPoint } from '../api/stockApi'

interface Props {
  data: OHLCVPoint[]
}

function formatShares(v: number): string {
  const abs = Math.abs(v)
  const sign = v < 0 ? '-' : ''
  if (abs >= 1_000_000) return `${sign}${(abs / 1_000_000).toFixed(1)}M`
  if (abs >= 1_000) return `${sign}${(abs / 1_000).toFixed(0)}K`
  return `${v.toFixed(0)}`
}

const CustomTooltip = ({ active, payload, label }: any) => {
  if (!active || !payload?.length) return null
  const d: OHLCVPoint = payload[0]?.payload
  return (
    <div className="bg-slate-800 border border-slate-600 rounded p-2 text-xs shadow-lg space-y-1">
      <p className="text-slate-300">{label}</p>
      <p>外資: <span className={Number(d.foreign_net ?? 0) >= 0 ? 'text-green-400' : 'text-red-400'}>{formatShares(Number(d.foreign_net ?? 0))}</span></p>
      <p>投信: <span className={Number(d.trust_net ?? 0) >= 0 ? 'text-green-400' : 'text-red-400'}>{formatShares(Number(d.trust_net ?? 0))}</span></p>
      <p>自營商: <span className={Number(d.dealer_net ?? 0) >= 0 ? 'text-green-400' : 'text-red-400'}>{formatShares(Number(d.dealer_net ?? 0))}</span></p>
      <p>合計: <span className={Number(d.institutional_net ?? 0) >= 0 ? 'text-green-300' : 'text-red-300'}>{formatShares(Number(d.institutional_net ?? 0))}</span></p>
      {d.institutional_as_of && (
        <p className="text-slate-500">資料日: {d.institutional_as_of}</p>
      )}
    </div>
  )
}

export const InstitutionalFlowChart: React.FC<Props> = ({ data }) => {
  const len = data.length
  const chartData = data.filter((d) => d.institutional_net != null)

  return (
    <ResponsiveContainer width="100%" height={190}>
      <ComposedChart data={chartData} margin={{ top: 4, right: 16, left: 0, bottom: 0 }}>
        <CartesianGrid strokeDasharray="3 3" stroke="#1e293b" />
        <XAxis
          dataKey="date"
          tick={{ fill: '#94a3b8', fontSize: 10 }}
          tickLine={false}
          tickFormatter={(val, idx) => {
            const step = Math.max(1, Math.floor(len / 10))
            return idx % step === 0 ? val : ''
          }}
          interval={0}
        />
        <YAxis
          tick={{ fill: '#94a3b8', fontSize: 10 }}
          tickLine={false}
          tickFormatter={formatShares}
          width={52}
        />
        <Tooltip content={<CustomTooltip />} />
        <Legend
          wrapperStyle={{ fontSize: 12 }}
          formatter={(value) => <span style={{ color: '#94a3b8' }}>{value}</span>}
        />
        <ReferenceLine y={0} stroke="#475569" strokeWidth={1} />
        <Bar dataKey="institutional_net" name="三大法人合計" isAnimationActive={false} maxBarSize={6}>
          {chartData.map((d, i) => (
            <Cell
              key={i}
              fill={Number(d.institutional_net ?? 0) >= 0 ? '#22c55e' : '#ef4444'}
              fillOpacity={0.72}
            />
          ))}
        </Bar>
        <Line
          dataKey="foreign_net"
          name="外資"
          stroke="#60a5fa"
          strokeWidth={1.4}
          dot={false}
          isAnimationActive={false}
          connectNulls
        />
        <Line
          dataKey="trust_net"
          name="投信"
          stroke="#facc15"
          strokeWidth={1.2}
          dot={false}
          isAnimationActive={false}
          connectNulls
        />
        <Line
          dataKey="dealer_net"
          name="自營商"
          stroke="#f472b6"
          strokeWidth={1.2}
          dot={false}
          isAnimationActive={false}
          connectNulls
        />
      </ComposedChart>
    </ResponsiveContainer>
  )
}