Skip to content

Commit

Permalink
refactor: better quote frame
Browse files Browse the repository at this point in the history
  • Loading branch information
er1c-zh committed Dec 6, 2024
1 parent 453a8a9 commit d83f82c
Show file tree
Hide file tree
Showing 8 changed files with 246 additions and 167 deletions.
41 changes: 22 additions & 19 deletions api/quote.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package api

import (
"asng/models"
"asng/models/value"
"asng/proto"
"asng/utils"
"fmt"
Expand All @@ -19,13 +20,16 @@ func (a *App) CandleStick(id models.StockIdentity, period proto.CandleStickPerio

type TodayQuoteResp struct {
models.BaseResp
Price []models.QuoteFrameDataSingleValue
AvgPrice []models.QuoteFrameDataSingleValue
Volume []models.QuoteFrameDataSingleValue
Frames []models.QuoteFrameRealtime
}

func (a *App) TodayQuote(id models.StockIdentity) TodayQuoteResp {
resp := TodayQuoteResp{}
resp.BaseResp = models.BaseResp{
Code: 0,
Message: "success",
}

var err error
tx, err := a.cli.TXToday(id)
if err != nil {
Expand All @@ -34,14 +38,17 @@ func (a *App) TodayQuote(id models.StockIdentity) TodayQuoteResp {
resp.Message = "fail"
return resp
}
resp.Price = make([]models.QuoteFrameDataSingleValue, 0)
resp.AvgPrice = make([]models.QuoteFrameDataSingleValue, 0)
resp.Volume = make([]models.QuoteFrameDataSingleValue, 0)

resp.Frames = make([]models.QuoteFrameRealtime, 0, len(tx))

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

var (
totalVolume int64 = 0
totalSum int64 = 0
)
for _, item := range tx {
if item.Tick == tickCur {
txOffsetInOneMinute += 3 // 3 seconds per tick
Expand All @@ -56,19 +63,15 @@ func (a *App) TodayQuote(id models.StockIdentity) TodayQuoteResp {
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,
Scale: 1,

totalVolume += item.Volume
totalSum += item.Price * item.Volume

resp.Frames = append(resp.Frames, models.QuoteFrameRealtime{
QuoteFrame: f,
Price: value.IntWithScale(item.Price, 10000),
AvgPrice: value.IntWithScale(totalSum/totalVolume, 10000),
Volume: value.IntWithScale(item.Volume, 1),
})
}
return resp
Expand Down
18 changes: 6 additions & 12 deletions api/subscribe.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package api

import (
"asng/models"
"asng/models/value"
"asng/proto"
"time"

Expand Down Expand Up @@ -34,8 +35,7 @@ func NewQuoteSubscripition(app *App) *QuoteSubscripition {

type QuoteSubscribeResp struct {
RealtimeInfo proto.RealtimeInfoRespItem
PriceFrame models.QuoteFrameDataSingleValue
VolumeFrame models.QuoteFrameDataSingleValue
Frame models.QuoteFrameRealtime
}

func (a *QuoteSubscripition) Start() {
Expand All @@ -60,19 +60,13 @@ func (a *App) Subscribe(req []models.StockIdentity) []QuoteSubscribeResp {
func (a *App) generateQuoteSubscribeResp(d proto.RealtimeInfoRespItem) QuoteSubscribeResp {
return QuoteSubscribeResp{
RealtimeInfo: d,
PriceFrame: models.QuoteFrameDataSingleValue{
Frame: models.QuoteFrameRealtime{
QuoteFrame: models.QuoteFrame{
TimeInMs: d.TickMilliSecTimestamp,
},
Value: d.CurrentPrice,
Scale: int64(a.stockMetaMap[d.ID].Scale),
},
VolumeFrame: models.QuoteFrameDataSingleValue{
QuoteFrame: models.QuoteFrame{
TimeInMs: d.TickMilliSecTimestamp,
},
Value: d.CurrentVolume,
Scale: 1,
Price: value.IntWithScale(d.CurrentPrice, int64(a.stockMetaMap[d.ID].Scale)),
AvgPrice: value.IntWithScale(int64(d.TotalAmount)/int64(d.TotalVolume), int64(a.stockMetaMap[d.ID].Scale)),
Volume: value.Int(d.CurrentVolume),
},
}
}
116 changes: 87 additions & 29 deletions frontend/src/components/RealtimeGraph.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { api, models, proto } from "../../wailsjs/go/models";
import { TodayQuote } from "../../wailsjs/go/api/App";
import { models, proto } from "../../wailsjs/go/models";
import * as d3 from "d3";
import { LogInfo } from "../../wailsjs/runtime/runtime";
import { formatPrice } from "./Viewer";

type RealtimeGraphProps = {
priceLine: models.QuoteFrameDataSingleValue[];
quote: models.QuoteFrameRealtime[];
meta: models.StockMetaItem;
realtime: proto.RealtimeInfoRespItem;
};

function RealtimeGraph(props: RealtimeGraphProps) {
Expand All @@ -29,25 +29,34 @@ function RealtimeGraph(props: RealtimeGraphProps) {
};
}, [containerRef.current]);

const ml = 40;
const mr = 20;
const ml = 45;
const mr = 45;
const mt = 20;
const mb = 20;
const svgRef = useRef<SVGSVGElement>(null);
const xAxisRef = useRef<SVGGElement>(null);
const yAxisRef = useRef<SVGGElement>(null);
const keyYAxisRef = useRef<SVGGElement>(null);
const lineGroupRef = useRef<SVGPathElement>(null);
const [priceRange, setPriceRange] = useState([0, 0]);

useEffect(() => {
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)));
let max = 0;
let min = 1e10;
props.quote.forEach((p) => {
max = Math.max(max, p.Price.V / p.Price.Scale);
min = Math.min(min, p.Price.V / p.Price.Scale);
});
const delta = Math.max(
Math.abs(props.realtime.YesterdayClose / props.meta.Scale - min),
Math.abs(max - props.realtime.YesterdayClose / props.meta.Scale)
);

setPriceRange([Math.min(...set), Math.max(...set)]);
}, [props.priceLine]);
setPriceRange([
props.realtime.YesterdayClose / props.meta.Scale - delta,
props.realtime.YesterdayClose / props.meta.Scale + delta,
]);
}, [props.meta, props.quote]);

const [scale, setScale] = useState({
X: d3.scaleTime(),
Expand Down Expand Up @@ -128,29 +137,64 @@ function RealtimeGraph(props: RealtimeGraphProps) {
.attr("stroke-opacity", 0.5)
.attr("stroke-dasharray", "2,2")
)
.call((g) => g.selectAll(".tick text").attr("x", -32).attr("dy", 2))
.call((g) => g.selectAll(".tick text").attr("x", -ml))
.call((g) => g.select(".domain").remove());

d3.select(keyYAxisRef.current!)
.call(
d3
.axisRight(scale.Y)
.tickValues(
[props.realtime.YesterdayClose / props.meta.Scale].concat(
priceRange
)
)
.tickSize(dimensions.width - ml - mr)
.tickFormat(d3.format(".2f"))
)
// .call((g) => g.selectAll(".tick text").attr("x", -32).attr("dy", 2))
.call((g) => g.select(".domain").remove());
}, [dimensions, scale]);

const lineBuilder = useCallback(
const priceLineBuilder = useCallback(
d3
.line<models.QuoteFrameDataSingleValue>()
.line<models.QuoteFrameRealtime>()
.x((d) => scale.X(new Date(d.TimeInMs)))
.y((d) => scale.Y(d.Value / d.Scale)),
.y((d) => scale.Y(d.Price.V / d.Price.Scale)),
[scale]
);
const avgPriceLineBuilder = useCallback(
d3
.line<models.QuoteFrameRealtime>()
.x((d) => scale.X(new Date(d.TimeInMs)))
.y((d) => scale.Y(d.AvgPrice.V / d.AvgPrice.Scale)),
[scale]
);

// draw price line
const pricePathRef = useRef<SVGLineElement>(null);
useEffect(() => {
if (!lineBuilder || props.priceLine.length == 0) {
if (!priceLineBuilder || props.quote.length == 0) {
return;
}
d3.select(pricePathRef.current).attr("d", lineBuilder(props.priceLine));
d3.select(pricePathRef.current).attr("d", priceLineBuilder(props.quote));
return () => {
d3.select(pricePathRef.current).attr("d", "");
};
}, [lineBuilder, props.priceLine, pricePathRef.current]);
}, [priceLineBuilder, props.quote, pricePathRef.current]);
// draw avg price line
const avgPricePathRef = useRef<SVGLineElement>(null);
useEffect(() => {
if (!avgPriceLineBuilder || props.quote.length == 0) {
return;
}
d3.select(avgPricePathRef.current).attr(
"d",
avgPriceLineBuilder(props.quote)
);
return () => {
d3.select(avgPricePathRef.current).attr("d", "");
};
}, [avgPriceLineBuilder, props.quote, avgPricePathRef.current]);

// draw crosshair
const crosshairRef = useRef<SVGGElement>(null);
Expand Down Expand Up @@ -208,31 +252,38 @@ function RealtimeGraph(props: RealtimeGraphProps) {
focused,
]);

const [frame, setFrame] = useState<models.QuoteFrameDataSingleValue>();
const [frame, setFrame] = useState<models.QuoteFrameRealtime>();
// set current frame
useEffect(() => {
if (!focused) {
setFrame(props.priceLine[0] ? props.priceLine[0] : undefined);
if (props.quote.length > 0) {
setFrame(props.quote[props.quote.length - 1]);
}
return;
}
const curXInMilliSec = scale.X.invert(curPos[0]).getTime();
let minDelta = curXInMilliSec;
let minDeltaIndex = 0;
props.priceLine.forEach((p, i) => {
props.quote.forEach((p, i) => {
if (Math.abs(p.TimeInMs - curXInMilliSec) < minDelta) {
minDelta = Math.abs(p.TimeInMs - curXInMilliSec);
minDeltaIndex = i;
}
});
setFrame(props.priceLine[minDeltaIndex]);
}, [curPos, scale, focused, props.priceLine]);
setFrame(props.quote[minDeltaIndex]);
}, [curPos, scale, focused, props.quote]);

return (
<div className="flex flex-col w-full h-full">
<div className="flex flex-row space-x-2 grow-0">
<div className="flex">分时图 {props.priceLine.length}</div>
<div>{frame ? new Date(frame.TimeInMs).toLocaleTimeString() : ""}</div>
<div>{frame ? (frame.Value / frame.Scale).toFixed(2) : ""}</div>
<div className="flex">分时图 {props.quote.length}</div>
<div>{frame ? new Date(frame.TimeInMs).toLocaleTimeString() : "-"}</div>
<div>
{frame ? formatPrice(frame.Price.V / frame.Price.Scale) : "-"}
</div>
<div className="text-yellow-500">
{frame ? formatPrice(frame.AvgPrice.V / frame.AvgPrice.Scale) : "-"}
</div>
</div>
<div ref={containerRef} className="flex grow">
<svg
Expand All @@ -245,13 +296,20 @@ function RealtimeGraph(props: RealtimeGraphProps) {
transform={`translate(0, ${dimensions.height - mb})`}
/>
<g ref={yAxisRef} transform={`translate(${ml}, 0)`} />
<g ref={keyYAxisRef} transform={`translate(${ml}, 0)`} />
<g ref={lineGroupRef}>
<path
ref={pricePathRef}
fill="none"
stroke="white"
strokeWidth={0.5}
/>
<path
ref={avgPricePathRef}
fill="none"
stroke="yellow"
strokeWidth={0.5}
/>
</g>
<g ref={crosshairRef} stroke="yellow">
<line ref={crosshairXRef} strokeWidth={0.5} />
Expand Down
Loading

0 comments on commit d83f82c

Please sign in to comment.