Кабинет: оценка портфеля по ценам QUIK и карточка бумаги из справочника депозитария

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
zuevav
2026-09-17 23:03:31 +03:00
parent f057b89d2e
commit 26785ff3ba
8 changed files with 614 additions and 21 deletions
+97 -5
View File
@@ -53,6 +53,40 @@ func New(cfg Config) (*Server, error) {
}
return strconv.FormatFloat(f, 'f', -1, 64)
},
"fmtmoney": func(f float64) string { // 1 234 567,89 — с разделителем разрядов
neg := f < 0
if neg {
f = -f
}
whole := int64(f)
cents := int64((f-float64(whole))*100 + 0.5)
if cents >= 100 {
whole, cents = whole+1, 0
}
d := strconv.FormatInt(whole, 10)
var b strings.Builder
for i, c := range d {
if i > 0 && (len(d)-i)%3 == 0 {
b.WriteByte(' ')
}
b.WriteRune(c)
}
out := b.String()
if cents > 0 {
out += "," + fmt.Sprintf("%02d", cents)
}
if neg {
out = "−" + out
}
return out
},
"fmtpct": func(f float64) string {
s := strconv.FormatFloat(f, 'f', 2, 64)
if f > 0 {
s = "+" + s
}
return strings.ReplaceAll(s, ".", ",") + " %"
},
"shortid": func(s string) string {
if len(s) > 8 {
return s[:8] + "…"
@@ -126,6 +160,9 @@ type position struct {
Available float64 `json:"available"`
UpdatedAt string `json:"updated_at"`
Transferable bool `json:"transferable"`
Price float64 `json:"price"`
Value float64 `json:"value"`
ChangePct float64 `json:"change_pct"`
Note string `json:"note"`
}
@@ -175,11 +212,42 @@ func claimsView(rows []claimRow) (active, done int) {
// portfolioResp — ответ /api/lk/portfolio.
type portfolioResp struct {
Contract string `json:"contract"`
FullName string `json:"full_name"`
Positions []position `json:"positions"`
Source string `json:"source"`
ReadOnly bool `json:"read_only"`
Contract string `json:"contract"`
FullName string `json:"full_name"`
Positions []position `json:"positions"`
Source string `json:"source"`
ReadOnly bool `json:"read_only"`
TotalValue float64 `json:"total_value"`
Priced int `json:"priced"`
}
// securityCard — карточка бумаги из ядра.
type securityCard struct {
ISIN string `json:"isin"`
Name string `json:"name"`
Kind string `json:"kind"`
Issuer string `json:"issuer"`
RegNumber string `json:"reg_number"`
CFI string `json:"cfi"`
Face float64 `json:"face"`
Maturity string `json:"maturity"`
Accepted bool `json:"accepted"`
QualOnly bool `json:"qual_only"`
QualNote string `json:"qual_note"`
Price float64 `json:"price"`
PriceIn string `json:"price_in"`
Board string `json:"board"`
ChangePct float64 `json:"change_pct"`
Position *position `json:"position"`
Payments []struct {
Date string `json:"date"`
Kind string `json:"kind"`
Amount float64 `json:"amount"`
Percent float64 `json:"percent"`
FixDate string `json:"fix_date"`
Paid bool `json:"paid"`
} `json:"payments"`
Note string `json:"note"`
}
// portfolio — реестр клиента из ядра.
@@ -324,6 +392,7 @@ func (s *Server) Handler() http.Handler {
"Nav": "b", "Contract": pf.Contract, "FullName": pf.FullName,
"Positions": pos, "Claims": claims, "Flash": r.URL.Query().Get("m"),
"Total": total, "Free": free, "Blocked": blocked, "Active": active,
"Value": pf.TotalValue, "Priced": pf.Priced,
})
})
@@ -349,9 +418,32 @@ func (s *Server) Handler() http.Handler {
"Positions": pf.Positions, "Flash": r.URL.Query().Get("m"),
"Source": pf.Source, "ReadOnly": pf.ReadOnly, "UpdatedAt": updated,
"Total": total, "Free": free, "Blocked": blocked,
"Value": pf.TotalValue, "Priced": pf.Priced,
})
})
// Карточка бумаги
mux.HandleFunc("/security", func(w http.ResponseWriter, r *http.Request) {
sess := sessionW(w, r)
if sess == "" {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
isin := strings.ToUpper(strings.TrimSpace(r.URL.Query().Get("isin")))
if isin == "" {
http.Redirect(w, r, "/portfolio", http.StatusSeeOther)
return
}
var out struct {
Security securityCard `json:"security"`
}
if err := bj.call(r.Context(), http.MethodGet, "/api/lk/security?isin="+url.QueryEscape(isin), sess, nil, &out); err != nil {
s.render(w, r, "security", map[string]any{"Nav": "p", "Error": err.Error(), "ISIN": isin})
return
}
s.render(w, r, "security", map[string]any{"Nav": "p", "S": out.Security, "ISIN": isin})
})
// Профиль: договор, счета депо, доступ
mux.HandleFunc("/profile", func(w http.ResponseWriter, r *http.Request) {
sess := sessionW(w, r)