Кабинет: архив не старше копии учёта (с пометкой), карточка бумаги без депозитарных реквизитов

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4YVo4UMopqT5DzicqpPjL
This commit is contained in:
zuevav
2026-09-18 15:56:48 +03:00
parent e7344d8121
commit 68e1153cb2
4 changed files with 39 additions and 10 deletions
+2 -1
View File
@@ -299,7 +299,8 @@ func registerLKAPI(mux *http.ServeMux, rc *RuntimeConfig, svc *Service) {
if err == nil && len(pf.Positions) == 0 && len(pf.Cash) == 0 { if err == nil && len(pf.Positions) == 0 && len(pf.Cash) == 0 {
// В торговой системе клиента нет (не торгует через QUIK) — текущий портфель из учёта. // В торговой системе клиента нет (не торгует через QUIK) — текущий портфель из учёта.
if ar, e := lkArchivePortfolio(r.Context(), fancy, u.Contract, time.Now()); e == nil { if ar, e := lkArchivePortfolio(r.Context(), fancy, u.Contract, time.Now()); e == nil {
ar.Note = "по данным брокерского учёта на сегодня" ar.Note = strings.TrimSpace(strings.TrimPrefix("в торговой системе позиций нет; портфель по брокерскому учёту на "+ar.AsOf+"; "+ar.Note, "; "))
ar.Note = strings.TrimSuffix(ar.Note, "; ")
pf = ar pf = ar
} }
} }
+33 -2
View File
@@ -21,6 +21,7 @@ import (
"fmt" "fmt"
"sort" "sort"
"strings" "strings"
"sync"
"time" "time"
) )
@@ -52,6 +53,13 @@ func lkArchivePortfolio(ctx context.Context, s FancyMSSQLSettings, contract stri
cctx, cancel := context.WithTimeout(ctx, 40*time.Second) cctx, cancel := context.WithTimeout(ctx, 40*time.Second)
defer cancel() defer cancel()
day := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, time.UTC) day := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, time.UTC)
// Мы читаем копию учёта; она может отставать. Позже её последнего дня данных нет —
// показываем портфель на последний день копии и говорим об этом.
note := ""
if last := spectreAsOf(cctx, conn); !last.IsZero() && day.After(last) {
day = last
note = "данные брокерского учёта доступны по " + last.Format("02.01.2006")
}
today := time.Now() today := time.Now()
isPast := day.Before(time.Date(today.Year(), today.Month(), today.Day(), 0, 0, 0, 0, time.UTC)) isPast := day.Before(time.Date(today.Year(), today.Month(), today.Day(), 0, 0, 0, 0, time.UTC))
@@ -110,7 +118,7 @@ func lkArchivePortfolio(ctx context.Context, s FancyMSSQLSettings, contract stri
} }
} }
out := &LKPortfolio{Source: "spectre", AsOf: day.Format("02.01.2006")} out := &LKPortfolio{Source: "spectre", AsOf: day.Format("02.01.2006"), Note: note}
fx := spectreFx(cctx, conn, day) fx := spectreFx(cctx, conn, day)
byISIN := map[string]*LKPosition{} byISIN := map[string]*LKPosition{}
cash := map[string]float64{} cash := map[string]float64{}
@@ -182,11 +190,34 @@ func lkArchivePortfolio(ctx context.Context, s FancyMSSQLSettings, contract stri
} }
lkPortfolioTotals(out) lkPortfolioTotals(out)
if c.Closed { if c.Closed {
out.Note = "договор закрыт" out.Note = strings.TrimSpace(strings.TrimPrefix(out.Note+"; договор закрыт", "; "))
} }
return out, nil return out, nil
} }
// spectreAsOf — последний день, за который в копии учёта есть документы (кэш 10 минут).
var spectreAsOfCache struct {
mu sync.Mutex
at time.Time
v time.Time
}
func spectreAsOf(ctx context.Context, conn *sql.DB) time.Time {
spectreAsOfCache.mu.Lock()
defer spectreAsOfCache.mu.Unlock()
if time.Since(spectreAsOfCache.at) < 10*time.Minute {
return spectreAsOfCache.v
}
var d sql.NullTime
_ = conn.QueryRowContext(ctx, `SELECT MAX(R_DATE) FROM OD_DOCS WHERE YEAR(R_DATE) < 9000`).Scan(&d)
v := time.Time{}
if d.Valid {
v = time.Date(d.Time.Year(), d.Time.Month(), d.Time.Day(), 0, 0, 0, 0, time.UTC)
}
spectreAsOfCache.at, spectreAsOfCache.v = time.Now(), v
return v
}
func abs(v float64) float64 { func abs(v float64) float64 {
if v < 0 { if v < 0 {
return -v return -v
+1 -1
View File
@@ -343,7 +343,7 @@ func TestBoardPages(t *testing.T) {
} }
} }
card := get("/security?isin=RU0009029540") card := get("/security?isin=RU0009029540")
for _, want := range []string{"ПАО Сбербанк", "Код CFI", "275,67", "дивиденды", "Перевести эту бумагу"} { for _, want := range []string{"ПАО Сбербанк", "275,67", "дивиденды", "Перевести эту бумагу", "← Портфель"} {
if !strings.Contains(card, want) { if !strings.Contains(card, want) {
t.Errorf("в карточке бумаги нет %q", want) t.Errorf("в карточке бумаги нет %q", want)
} }
+3 -6
View File
@@ -440,9 +440,9 @@ input[type=checkbox]{width:20px;min-height:20px;height:20px}
{{$s := .S}} {{$s := .S}}
<section class="board"><div class="board-row"> <section class="board"><div class="board-row">
<div> <div>
<a href="/portfolio" class="lbl" style="color:var(--dim);text-decoration:none">← Реестр</a> <a href="/" class="lbl" style="color:var(--dim);text-decoration:none">← Портфель</a>
<div style="font-size:23px;font-weight:700;margin-top:8px">{{if $s.Name}}{{$s.Name}}{{else}}{{.ISIN}}{{end}}</div> <div style="font-size:23px;font-weight:700;margin-top:8px">{{if $s.Name}}{{$s.Name}}{{else}}{{.ISIN}}{{end}}</div>
<div class="n" style="font-size:13px;color:var(--dim);margin-top:6px">{{.ISIN}}{{with $s.Kind}} · {{.}}{{end}}{{with $s.RegNumber}} · {{.}}{{end}}</div> <div class="n" style="font-size:13px;color:var(--dim);margin-top:6px">{{.ISIN}}{{with $s.Kind}} · {{.}}{{end}}</div>
</div> </div>
<div class="stats"> <div class="stats">
{{with $s.Position}}<span class="stat"><span class="lbl" style="color:var(--dim)">В портфеле</span><span class="v">{{fmtnum .Quantity}}</span></span> {{with $s.Position}}<span class="stat"><span class="lbl" style="color:var(--dim)">В портфеле</span><span class="v">{{fmtnum .Quantity}}</span></span>
@@ -462,10 +462,7 @@ input[type=checkbox]{width:20px;min-height:20px;height:20px}
{{with $s.Kind}}<div class="row"><span>Вид бумаги</span><span>{{.}}</span></div>{{end}} {{with $s.Kind}}<div class="row"><span>Вид бумаги</span><span>{{.}}</span></div>{{end}}
{{if gt $s.Face 0.0}}<div class="row"><span>Номинал</span><span class="n">{{fmtmoney $s.Face}} ₽</span></div>{{end}} {{if gt $s.Face 0.0}}<div class="row"><span>Номинал</span><span class="n">{{fmtmoney $s.Face}} ₽</span></div>{{end}}
{{with $s.Maturity}}<div class="row"><span>Погашение</span><span class="n">{{.}}</span></div>{{end}} {{with $s.Maturity}}<div class="row"><span>Погашение</span><span class="n">{{.}}</span></div>{{end}}
{{with $s.RegNumber}}<div class="row"><span>Государственная регистрация</span><span class="n">{{.}}</span></div>{{end}} {{if $s.QualOnly}}<div class="row"><span>Доступ</span><span>только для квалифицированных инвесторов</span></div>{{end}}
{{with $s.CFI}}<div class="row"><span>Код CFI</span><span class="n">{{.}}</span></div>{{end}}
<div class="row"><span>Обслуживание в депозитарии</span><span>{{if $s.Accepted}}принята{{else}}не принята{{end}}</span></div>
<div class="row"><span>Для квалифицированных инвесторов</span><span>{{if $s.QualOnly}}да{{else}}ограничений нет{{end}}</span></div>
{{if gt $s.Price 0.0}}<div class="row"><span>Цена{{with $s.Board}} · {{.}}{{end}}</span><span class="n">{{fmtmoney $s.Price}}{{with $s.PriceIn}} <span style="color:var(--mut)">{{.}}</span>{{end}}</span></div>{{end}} {{if gt $s.Price 0.0}}<div class="row"><span>Цена{{with $s.Board}} · {{.}}{{end}}</span><span class="n">{{fmtmoney $s.Price}}{{with $s.PriceIn}} <span style="color:var(--mut)">{{.}}</span>{{end}}</span></div>{{end}}
</div> </div>
{{with $s.Position}}{{if and .Transferable (gt .Available 0.0)}} {{with $s.Position}}{{if and .Transferable (gt .Available 0.0)}}