PSC daily candle history and pixel candlestick chart
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7b5080984a
commit
b8308e1010
10 files changed
+271
-4
No files matched your search
@@ -0,0 +1,115 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Decentralized.Domain;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Decentralized
|
||||
{
|
||||
/// <summary>
|
||||
/// 日 K 图:运行时画到一张小 Texture2D 上(一根蜡烛 5 像素宽、间隔 1),点采样放大 2 倍显示,和游戏的像素风格一致。
|
||||
/// 上面是价格(涨绿跌红、影线、7 日均线、可选阈值虚线),下面是成交量(玩家青色叠在大户灰色上)。纹理按用途缓存复用。
|
||||
/// </summary>
|
||||
public static class CandleChart
|
||||
{
|
||||
public const int Days = 30, Step = 6, Body = 5, PriceH = 70, Gap = 3, VolH = 16, Pad = 2;
|
||||
public const int W = Days * Step + Pad * 2, H = PriceH + Gap + VolH + Pad * 2;
|
||||
public const float Scale = 2f;
|
||||
|
||||
private static readonly Color32 Bg = new Color32(30, 31, 34, 255), Grid = new Color32(48, 50, 54, 255);
|
||||
private static readonly Color32 Up = new Color32(108, 190, 112, 255), Down = new Color32(214, 94, 88, 255);
|
||||
private static readonly Color32 Ma = new Color32(226, 196, 92, 255), Thr = new Color32(150, 170, 230, 255);
|
||||
private static readonly Color32 VolNpc = new Color32(92, 96, 104, 255), VolMe = new Color32(96, 200, 210, 255);
|
||||
|
||||
private sealed class Slot { public Texture2D Tex; public Sprite Sprite; public Color32[] Px; }
|
||||
private static readonly Dictionary<string, Slot> Slots = new Dictionary<string, Slot>();
|
||||
|
||||
/// <summary>画一张图,返回可以交给 AddImage 的 Sprite。threshold > 0 时画一条水平虚线(预测盘的阈值)。</summary>
|
||||
public static Sprite Render(string key, IList<Candle> all, double threshold = 0)
|
||||
{
|
||||
if (!Slots.TryGetValue(key, out var s) || s.Tex == null)
|
||||
{
|
||||
var tex = new Texture2D(W, H, TextureFormat.RGBA32, false) { filterMode = FilterMode.Point, wrapMode = TextureWrapMode.Clamp };
|
||||
s = Slots[key] = new Slot { Tex = tex, Px = new Color32[W * H], Sprite = Sprite.Create(tex, new Rect(0, 0, W, H), new Vector2(0.5f, 0.5f), 100f) };
|
||||
}
|
||||
var px = s.Px;
|
||||
for (int i = 0; i < px.Length; i++) px[i] = Bg;
|
||||
var c = all.Skip(Math.Max(0, all.Count - Days)).ToList();
|
||||
int volY0 = Pad, priceY0 = Pad + VolH + Gap;
|
||||
// 网格:价格区四等分
|
||||
for (int g = 1; g < 4; g++) HLine(px, Pad, W - Pad - 1, priceY0 + PriceH * g / 4, Grid);
|
||||
if (c.Count > 0)
|
||||
{
|
||||
var ma = CandleBook.Ma(all, 7).Skip(Math.Max(0, all.Count - Days)).ToList();
|
||||
double lo = Math.Min(c.Min(x => x.L), ma.Min()), hi = Math.Max(c.Max(x => x.H), ma.Max());
|
||||
if (threshold > 0) { lo = Math.Min(lo, threshold); hi = Math.Max(hi, threshold); }
|
||||
double span = Math.Max(hi - lo, hi * 0.02);
|
||||
lo -= span * 0.05; hi += span * 0.05; span = hi - lo;
|
||||
Func<double, int> y = p => priceY0 + (int)Math.Round((p - lo) / span * (PriceH - 1));
|
||||
double vmax = Math.Max(1, c.Max(x => x.VolPlayer + x.VolNpc));
|
||||
int x0 = Pad + (Days - c.Count) * Step; // 不足 30 根时靠右
|
||||
for (int i = 0; i < c.Count; i++)
|
||||
{
|
||||
var k = c[i];
|
||||
int left = x0 + i * Step, mid = left + Body / 2;
|
||||
var col = k.C >= k.O ? Up : Down;
|
||||
VLine(px, mid, y(k.L), y(k.H), col);
|
||||
int b0 = y(Math.Min(k.O, k.C)), b1 = Math.Max(b0, y(Math.Max(k.O, k.C)));
|
||||
for (int xx = left; xx < left + Body; xx++)
|
||||
{
|
||||
// 当天还没收盘的蜡烛画成空心
|
||||
if (!k.Closed && xx > left && xx < left + Body - 1 && b1 - b0 > 1) { Set(px, xx, b0, col); Set(px, xx, b1, col); continue; }
|
||||
VLine(px, xx, b0, b1, col);
|
||||
}
|
||||
int hn = (int)Math.Round(k.VolNpc / vmax * VolH), hm = (int)Math.Round(k.VolPlayer / vmax * VolH);
|
||||
for (int xx = left; xx < left + Body; xx++)
|
||||
{
|
||||
if (hn > 0) VLine(px, xx, volY0, volY0 + hn - 1, VolNpc);
|
||||
if (hm > 0) VLine(px, xx, volY0 + hn, volY0 + hn + hm - 1, VolMe);
|
||||
}
|
||||
}
|
||||
for (int i = 1; i < ma.Count; i++) Line(px, x0 + (i - 1) * Step + Body / 2, y(ma[i - 1]), x0 + i * Step + Body / 2, y(ma[i]), Ma);
|
||||
if (threshold > 0) { int ty = y(threshold); for (int xx = Pad; xx < W - Pad; xx++) if ((xx / 3) % 2 == 0) Set(px, xx, ty, Thr); }
|
||||
}
|
||||
s.Tex.SetPixels32(px);
|
||||
s.Tex.Apply(false);
|
||||
return s.Sprite;
|
||||
}
|
||||
|
||||
/// <summary>图上方的一行说明:最高、最低、最新收盘,涨跌。</summary>
|
||||
public static string Caption(IList<Candle> all)
|
||||
{
|
||||
var c = all.Skip(Math.Max(0, all.Count - Days)).ToList();
|
||||
if (c.Count == 0) return L.T("chart.empty");
|
||||
var last = c[c.Count - 1];
|
||||
double prev = c.Count > 1 ? c[c.Count - 2].C : last.O, chg = prev > 0 ? last.C / prev - 1 : 0;
|
||||
return L.T("chart.caption", c.Count, Fmt.Num(c.Max(x => x.H)), Fmt.Num(c.Min(x => x.L)), Fmt.Num(last.C), (chg * 100).ToString("+0.0;-0.0;0.0", CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
private static void Set(Color32[] px, int x, int y, Color32 c) { if (x >= 0 && x < W && y >= 0 && y < H) px[y * W + x] = c; }
|
||||
private static void HLine(Color32[] px, int x0, int x1, int y, Color32 c) { for (int x = x0; x <= x1; x++) Set(px, x, y, c); }
|
||||
private static void VLine(Color32[] px, int x, int y0, int y1, Color32 c) { if (y1 < y0) (y0, y1) = (y1, y0); for (int y = y0; y <= y1; y++) Set(px, x, y, c); }
|
||||
|
||||
private static void Line(Color32[] px, int x0, int y0, int x1, int y1, Color32 c)
|
||||
{
|
||||
int dx = Math.Abs(x1 - x0), dy = -Math.Abs(y1 - y0), sx = x0 < x1 ? 1 : -1, sy = y0 < y1 ? 1 : -1, err = dx + dy;
|
||||
while (true)
|
||||
{
|
||||
Set(px, x0, y0, c);
|
||||
if (x0 == x1 && y0 == y1) break;
|
||||
int e2 = 2 * err;
|
||||
if (e2 >= dy) { err += dy; x0 += sx; }
|
||||
if (e2 <= dx) { err += dx; y0 += sy; }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>把图放进界面:说明一行、图、图例一行。</summary>
|
||||
public static void Add(CustomUIBuilder b, string key, IList<Candle> all, double threshold = 0)
|
||||
{
|
||||
b.AddLabel(Caption(all));
|
||||
b.AddImage(Render(key, all, threshold), W * Scale, H * Scale);
|
||||
b.AddLabel(L.T(threshold > 0 ? "chart.legend.thr" : "chart.legend"));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user