Skip to content

Commit

Permalink
feat: realtime graph use transaction data now
Browse files Browse the repository at this point in the history
  • Loading branch information
er1c-zh committed Dec 4, 2024
1 parent 1939de7 commit 89b00ba
Show file tree
Hide file tree
Showing 9 changed files with 270 additions and 135 deletions.
57 changes: 53 additions & 4 deletions api/quote.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package api
import (
"asng/models"
"asng/proto"
"asng/utils"
"fmt"
"time"
)

func (a *App) CandleStick(id models.StockIdentity, period proto.CandleStickPeriodType, cursor uint16) *proto.CandleStickResp {
Expand All @@ -15,13 +17,60 @@ func (a *App) CandleStick(id models.StockIdentity, period proto.CandleStickPerio
return resp
}

func (a *App) TodayQuote(id models.StockIdentity) []proto.QuoteFrame {
resp, err := a.cli.RealtimeGraph(id)
type TodayQuoteResp struct {
models.BaseResp
Price []models.QuoteFrameDataSingleValue
AvgPrice []models.QuoteFrameDataSingleValue
Volume []models.QuoteFrameDataSingleValue
}

func (a *App) TodayQuote(id models.StockIdentity) TodayQuoteResp {
resp := TodayQuoteResp{}
var err error
tx, err := a.cli.TXToday(id)
if err != nil {
a.LogProcessError(models.ProcessInfo{Msg: fmt.Sprintf("today quote failed: %s", err.Error())})
return nil
resp.Code = 1
resp.Message = "fail"
return resp
}
return resp.List
resp.Price = make([]models.QuoteFrameDataSingleValue, 0)
resp.AvgPrice = make([]models.QuoteFrameDataSingleValue, 0)
resp.Volume = make([]models.QuoteFrameDataSingleValue, 0)

t0 := utils.GetTodayWithOffset(0, 0, 0)
txOffsetInOneMinute := 0
tickCur := uint16(0)

for _, item := range tx {
if item.Tick == tickCur {
txOffsetInOneMinute += 3 // 3 seconds per tick
} else {
tickCur = item.Tick
txOffsetInOneMinute = 0
}
f := models.QuoteFrame{
TimeInMs: t0.Add(
time.Duration(item.Tick)*time.Minute +
time.Duration(txOffsetInOneMinute)*time.Second).
UnixMilli(),
TimeSpanInMs: time.Second.Milliseconds(),
}
resp.Price = append(resp.Price, models.QuoteFrameDataSingleValue{
QuoteFrame: f.Clone().SetType(models.QuoteTypeLine),
Value: item.Price,
Scale: 10000,
})
// resp.AvgPrice = append(resp.AvgPrice, models.QuoteFrameDataSingleValue{
// QuoteFrame: f.Clone().SetType(models.QuoteTypeLine),
// Value: item.AvgPrice,
// })
// resp.Volume = append(resp.Volume, models.QuoteFrameDataSingleValue{
// QuoteFrame: f.Clone().SetType(models.QuoteTypeBar),
// Value: item.Volume,
// })
}
return resp
}

func (a *App) RealtimeInfo(req []models.StockIdentity) *proto.RealtimeInfoResp {
Expand Down
6 changes: 3 additions & 3 deletions examples/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,9 @@ func testSubscribe() {
// resp, err := cli.TXToday(models.StockIdentity{
// MarketType: models.MarketSH, Code: "600000",
// })
// resp, err := cli.TXRealtime(models.StockIdentity{
// MarketType: models.MarketSH, Code: "600000"})
resp, err := cli.CandleStick(models.StockIdentity{MarketType: models.MarketSH, Code: "600000"}, proto.CandleStickPeriodType_Day, 0)
resp, err := cli.TXRealtime(models.StockIdentity{
MarketType: models.MarketSH, Code: "600000"}, 0)
// resp, err := cli.CandleStick(models.StockIdentity{MarketType: models.MarketSH, Code: "600000"}, proto.CandleStickPeriodType_Day, 0)
if err != nil {
fmt.Printf("error:%s", err)
return
Expand Down
166 changes: 62 additions & 104 deletions frontend/src/components/RealtimeGraph.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
import { useEffect, useRef, useState } from "react";
import { models, proto } from "../../wailsjs/go/models";
import { api, models, proto } from "../../wailsjs/go/models";
import { TodayQuote } from "../../wailsjs/go/api/App";
import * as d3 from "d3";
import { LogInfo } from "../../wailsjs/runtime/runtime";
import { formatPrice } from "./Viewer";

type RealtimeGraphProps = {
id: models.StockIdentity;
realtimeData: proto.RealtimeInfoRespItem | undefined;
priceLine: models.QuoteFrameDataSingleValue[];
};

function RealtimeGraphProps(props: RealtimeGraphProps) {
const containerRef = useRef<HTMLDivElement>(null);
const [dimensions, setDimensions] = useState({
width: 0,
height: 0,
});
const [data, setData] = useState<proto.QuoteFrame[]>([]);
const [cursorX, setCursorX] = useState(0);

useEffect(() => {
const resizeObserver = new ResizeObserver((entries) => {
Expand All @@ -31,16 +29,6 @@ function RealtimeGraphProps(props: RealtimeGraphProps) {
};
}, [containerRef.current]);

useEffect(() => {
if (!props.id.Code) {
return;
}
TodayQuote(props.id).then((data) => {
setData(data);
setCursorX(data.length - 1);
});
}, [props.id]);

const ml = 40;
const mr = 20;
const mt = 20;
Expand All @@ -49,62 +37,69 @@ function RealtimeGraphProps(props: RealtimeGraphProps) {
const xAxisRef = useRef<SVGGElement>(null);
const yAxisRef = useRef<SVGGElement>(null);
const lineGroupRef = useRef<SVGPathElement>(null);
const [priceRange, setPriceRange] = useState([0, 0]);

useEffect(() => {
if (!data || !props.realtimeData) {
return;
}
const yesterdayClose =
(props.realtimeData.CurrentPrice +
props.realtimeData.YesterdayCloseDelta) /
100.0;
let set = [];

// max and min price in price line
set.push(Math.max(...props.priceLine.map((p) => p.Value / p.Scale)));
set.push(Math.min(...props.priceLine.map((p) => p.Value / p.Scale)));

console.log(set);

setPriceRange([Math.min(...set), Math.max(...set)]);
}, [props.priceLine]);

useEffect(() => {
const widthPer5Min = (dimensions.width - ml - mr) / 50;
const xScale = d3
.scaleLinear()
.domain([0, 240])
.range([ml, dimensions.width - mr]);
const yScale = d3
.scaleLinear()
.scaleTime()
.domain([
Math.min(
...data.map((d) => {
return yesterdayClose - Math.abs(d.Price / 100.0 - yesterdayClose);
})
) -
0.01 * yesterdayClose,
Math.max(
...data.map((d) => {
return yesterdayClose + Math.abs(d.Price / 100.0 - yesterdayClose);
})
) +
0.01 * yesterdayClose,
new Date().setHours(9, 15, 0, 0),
new Date().setHours(9, 25, 0, 0),
new Date().setHours(9, 30, 0, 0),
new Date().setHours(11, 30, 0, 0),
new Date().setHours(13, 0, 0, 0),
new Date().setHours(15, 0, 0, 0),
])
.range([
ml,
ml + widthPer5Min * 2,
ml + widthPer5Min * 2,
ml + widthPer5Min * (2 + 24),
ml + widthPer5Min * (2 + 24),
dimensions.width - mr,
]);
const yScale = d3
.scaleLinear()
.domain(priceRange)
.range([dimensions.height - mb, mt]);

d3.select(xAxisRef.current!)
.call(
d3
.axisTop(xScale)
.tickSize(dimensions.height - mt - mb)
.tickValues(
[0 /* FIXME d3 bug? */].concat([
0, 30, 60, 90, 120, 150, 180, 210, 240,
])
)
.tickValues([
new Date().setHours(9, 15, 0, 0), // TODO: d3 don't show first value, is this d3 bug?
new Date().setHours(9, 15, 0, 0),
new Date().setHours(9, 30, 0, 0),
new Date().setHours(10, 0, 0, 0),
new Date().setHours(10, 30, 0, 0),
new Date().setHours(11, 0, 0, 0),
new Date().setHours(11, 30, 0, 0),
new Date().setHours(13, 30, 0, 0),
new Date().setHours(14, 0, 0, 0),
new Date().setHours(14, 30, 0, 0),
new Date().setHours(15, 0, 0, 0),
])
.tickFormat((d) => {
if (d.valueOf() < 120) {
return (
9 +
Math.floor((d.valueOf() + 30) / 60) +
":" +
d3.format("02d")((d.valueOf() + 30) % 60)
);
} else if (d.valueOf() == 120) {
const t0 = new Date(d.valueOf());
if (t0.getHours() == 11 && t0.getMinutes() == 30) {
return "11:30/13:00";
} else {
return (
13 +
Math.floor((d.valueOf() - 120) / 60) +
":" +
d3.format("02d")((d.valueOf() - 120) % 60)
);
return d3.timeFormat("%H:%M")(t0);
}
})
)
Expand All @@ -119,17 +114,6 @@ function RealtimeGraphProps(props: RealtimeGraphProps) {
.axisRight(yScale)
.tickSize(dimensions.width - ml - mr)
.tickFormat(d3.format(".2f"))
.tickValues(
yScale
.domain()
.concat(
Array.from({ length: 7 }, (_, i) => i).map(
(i) =>
yesterdayClose +
(i - 3) * ((yScale.domain()[1] - yesterdayClose) / 4)
)
)
)
)
.call((g) =>
g
Expand All @@ -140,53 +124,27 @@ function RealtimeGraphProps(props: RealtimeGraphProps) {
.call((g) => g.selectAll(".tick text").attr("x", -32).attr("dy", 2))
.call((g) => g.select(".domain").remove());

const lineZero = d3
.line<Number>()
.x((d, i) => xScale(i))
.y((d) => yScale(d));
d3.select(lineGroupRef.current!)
.append("path")
.attr(
"d",
lineZero(Array.from({ length: 240 }, (_, i) => yesterdayClose))
)
.attr("fill", "none")
.attr("stroke", "white");
const linePrice = d3
.line<models.QuoteFrameDataSingleValue>()
.x((d) => xScale(new Date(d.TimeInMs)))
.y((d) => yScale(d.Value / d.Scale));

const lineRealtime = d3
.line<proto.QuoteFrame>()
.x((d, i) => xScale(i))
.y((d) => yScale(d.Price / 100));
d3.select(lineGroupRef.current!)
.append("path")
.attr("d", lineRealtime(data!))
.attr("fill", "none")
.attr("stroke", "white");
const lineAvg = d3
.line<proto.QuoteFrame>()
.x((d, i) => xScale(i))
.y((d) => yScale(d.AvgPrice / 10000));
d3.select(lineGroupRef.current!)
.append("path")
.attr("d", lineAvg(data!))
.attr("fill", "none")
.attr("stroke", "yellow");
.attr("stroke", "white")
.attr("stroke-width", 0.5)
.attr("d", linePrice(props.priceLine));

return () => {
d3.select(lineGroupRef.current!).selectAll("*").remove();
};
}, [data, props.realtimeData]);
}, [props.priceLine, priceRange]);

return (
<div className="flex flex-col w-full h-full">
<div className="flex flex-row space-x-2 grow-0">
<div className="flex">分时图</div>
<div className="flex">{props.id.Code}</div>
<div className="flex">
现价 {formatPrice(data[cursorX]?.Price, 100)}
</div>
<div className="flex text-yellow-400">
均价 {formatPrice(data[cursorX]?.AvgPrice, 10000)}
</div>
</div>
<div ref={containerRef} className="flex grow">
<svg
Expand Down
Loading

0 comments on commit 89b00ba

Please sign in to comment.