ninth version
Deploy / Test, build and deploy (push) Failing after 1m6s

This commit is contained in:
2026-06-08 14:25:44 +00:00
parent e8b7d8e27c
commit 20cc8506ad
21 changed files with 847 additions and 148 deletions
+161 -15
View File
@@ -306,13 +306,19 @@ func (g *RealGateway) GetPortfolio(ctx context.Context, accountID string) (domai
if err != nil {
return domain.Portfolio{}, err
}
return portfolioFromResponse(resp, g.lotForInstrument)
return portfolioFromResponse(resp, func(instrumentUID string) (int64, error) {
return g.resolveInstrumentLot(ctx, instrumentUID)
})
}
func (g *RealGateway) GetOperations(ctx context.Context, accountID string, from, to time.Time) ([]domain.Operation, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
ops, err := g.getOperationsByCursor(ctx, accountID, from, to)
if err == nil {
return ops, nil
}
resp, err := requestWithTimeout(ctx, g.requestTimeout, func(callCtx context.Context) (*pb.OperationsResponse, error) {
return retryValue(callCtx, g.retryAttempts, g.retryBackoff, func() (*pb.OperationsResponse, error) {
return g.operationsPB.GetOperations(callCtx, &pb.OperationsRequest{
@@ -328,30 +334,122 @@ func (g *RealGateway) GetOperations(ctx context.Context, accountID string, from,
return operationsFromResponse(resp), nil
}
func (g *RealGateway) getOperationsByCursor(ctx context.Context, accountID string, from, to time.Time) ([]domain.Operation, error) {
limit := int32(1000)
withoutCommissions := false
withoutTrades := true
withoutOvernights := false
state := pb.OperationState_OPERATION_STATE_EXECUTED
var cursor *string
var out []domain.Operation
for {
resp, err := requestWithTimeout(ctx, g.requestTimeout, func(callCtx context.Context) (*pb.GetOperationsByCursorResponse, error) {
return retryValue(callCtx, g.retryAttempts, g.retryBackoff, func() (*pb.GetOperationsByCursorResponse, error) {
return g.operationsPB.GetOperationsByCursor(callCtx, &pb.GetOperationsByCursorRequest{
AccountId: accountID,
From: investgo.TimeToTimestamp(from),
To: investgo.TimeToTimestamp(to),
Cursor: cursor,
Limit: &limit,
State: &state,
WithoutCommissions: &withoutCommissions,
WithoutTrades: &withoutTrades,
WithoutOvernights: &withoutOvernights,
})
})
})
if err != nil {
return nil, err
}
out = append(out, operationsFromCursorResponse(resp)...)
if !resp.GetHasNext() || resp.GetNextCursor() == "" {
return out, nil
}
next := resp.GetNextCursor()
cursor = &next
}
}
func operationsFromResponse(resp *pb.OperationsResponse) []domain.Operation {
ops := resp.GetOperations()
out := make([]domain.Operation, 0, len(ops))
for _, op := range ops {
payment := money.MoneyValueToDecimal(op.GetPayment())
instrumentUID := op.GetInstrumentUid()
commission := operationCommission(op.GetOperationType(), payment)
childCommission := decimal.Zero
for _, child := range op.GetChildOperations() {
childPayment := money.MoneyValueToDecimal(child.GetPayment())
if instrumentUID == "" {
instrumentUID = child.GetInstrumentUid()
}
childCommission = childCommission.Add(operationCommission(op.GetOperationType(), childPayment))
}
if commission.IsZero() {
commission = childCommission
}
out = append(out, domain.Operation{
ID: op.GetId(),
InstrumentUID: op.GetInstrumentUid(),
InstrumentUID: instrumentUID,
Type: op.GetOperationType().String(),
Payment: payment,
Commission: operationCommission(op.GetOperationType(), payment),
Commission: commission,
ExecutedAt: op.GetDate().AsTime().UTC(),
})
}
return out
}
func portfolioFromResponse(resp *pb.PortfolioResponse, lotForInstrument func(string) int64) (domain.Portfolio, error) {
func operationsFromCursorResponse(resp *pb.GetOperationsByCursorResponse) []domain.Operation {
items := resp.GetItems()
out := make([]domain.Operation, 0, len(items))
for _, item := range items {
payment := money.MoneyValueToDecimal(item.GetPayment())
commission := money.Abs(money.MoneyValueToDecimal(item.GetCommission()))
instrumentUID := item.GetInstrumentUid()
childCommission := decimal.Zero
for _, child := range item.GetChildOperations() {
childPayment := money.Abs(money.MoneyValueToDecimal(child.GetPayment()))
if instrumentUID == "" {
instrumentUID = child.GetInstrumentUid()
}
if operationLooksLikeCommission(item.GetType(), childPayment) {
childCommission = childCommission.Add(childPayment)
}
}
if commission.IsZero() && operationLooksLikeCommission(item.GetType(), payment) {
commission = money.Abs(payment)
}
if commission.IsZero() {
commission = childCommission
}
out = append(out, domain.Operation{
ID: item.GetId(),
InstrumentUID: instrumentUID,
Type: item.GetType().String(),
Payment: payment,
Commission: commission,
ExecutedAt: item.GetDate().AsTime().UTC(),
})
}
return out
}
func portfolioFromResponse(resp *pb.PortfolioResponse, lotForInstrument func(string) (int64, error)) (domain.Portfolio, error) {
positions := resp.GetPositions()
holdings := make([]domain.Holding, 0, len(positions))
for _, position := range positions {
if portfolioPositionIgnored(position) {
continue
}
lot, lotErr := portfolioPositionLot(position, lotForInstrument)
lots, err := portfolioQuantityLots(position, lot, lotErr)
if err != nil {
return domain.Portfolio{}, err
}
holdings = append(holdings, domain.Holding{
InstrumentUID: position.GetInstrumentUid(),
QuantityLots: portfolioQuantityLots(position, portfolioPositionLot(position, lotForInstrument)),
QuantityLots: lots,
AveragePrice: money.MoneyValueToDecimal(position.GetAveragePositionPrice()),
MarketValue: money.MoneyValueToDecimal(position.GetCurrentPrice()).Mul(money.QuotationToDecimal(position.GetQuantity())),
})
@@ -396,14 +494,22 @@ func (g *RealGateway) GetServerTime(ctx context.Context) (time.Time, error) {
}
func operationCommission(operationType pb.OperationType, payment decimal.Decimal) decimal.Decimal {
if operationType != pb.OperationType_OPERATION_TYPE_BROKER_FEE &&
operationType != pb.OperationType_OPERATION_TYPE_SERVICE_FEE &&
operationType != pb.OperationType_OPERATION_TYPE_SUCCESS_FEE {
if !operationTypeIsCommission(operationType) {
return decimal.Zero
}
return money.Abs(payment)
}
func operationTypeIsCommission(operationType pb.OperationType) bool {
return operationType == pb.OperationType_OPERATION_TYPE_BROKER_FEE ||
operationType == pb.OperationType_OPERATION_TYPE_SERVICE_FEE ||
operationType == pb.OperationType_OPERATION_TYPE_SUCCESS_FEE
}
func operationLooksLikeCommission(operationType pb.OperationType, payment decimal.Decimal) bool {
return operationTypeIsCommission(operationType) && !payment.IsZero()
}
func rubMoneyValueToDecimal(value *pb.MoneyValue) (decimal.Decimal, error) {
if value == nil {
return decimal.Zero, nil
@@ -414,25 +520,38 @@ func rubMoneyValueToDecimal(value *pb.MoneyValue) (decimal.Decimal, error) {
return money.MoneyValueToDecimal(value), nil
}
func portfolioPositionLot(position *pb.PortfolioPosition, lotForInstrument func(string) int64) int64 {
func portfolioPositionLot(position *pb.PortfolioPosition, lotForInstrument func(string) (int64, error)) (int64, error) {
if position == nil || lotForInstrument == nil {
return 0
return 0, nil
}
return lotForInstrument(position.GetInstrumentUid())
}
func portfolioQuantityLots(position *pb.PortfolioPosition, lot int64) int64 {
func portfolioPositionIgnored(position *pb.PortfolioPosition) bool {
if position == nil {
return 0
return true
}
if money.QuotationToDecimal(position.GetQuantity()).IsZero() {
return true
}
return strings.EqualFold(position.GetInstrumentType(), "currency")
}
func portfolioQuantityLots(position *pb.PortfolioPosition, lot int64, lotErr error) (int64, error) {
if position == nil {
return 0, nil
}
if lots, ok := portfolioDeprecatedQuantityLots(position); ok {
return lots.IntPart()
return lots.IntPart(), nil
}
if lotErr != nil {
return 0, lotErr
}
quantity := money.QuotationToDecimal(position.GetQuantity())
if lot > 0 {
return quantity.Div(decimal.NewFromInt(lot)).IntPart()
return quantity.Div(decimal.NewFromInt(lot)).IntPart(), nil
}
return quantity.IntPart()
return 0, fmt.Errorf("portfolio lot size is unknown for %s", position.GetInstrumentUid())
}
func (g *RealGateway) storeInstrumentLot(instrument domain.Instrument) {
@@ -457,6 +576,33 @@ func (g *RealGateway) lotForInstrument(instrumentUID string) int64 {
return lot
}
func (g *RealGateway) resolveInstrumentLot(ctx context.Context, instrumentUID string) (int64, error) {
if lot := g.lotForInstrument(instrumentUID); lot > 0 {
return lot, nil
}
if instrumentUID == "" {
return 0, errors.New("portfolio instrument uid is empty")
}
resp, err := requestWithTimeout(ctx, g.requestTimeout, func(callCtx context.Context) (*pb.InstrumentResponse, error) {
return retryValue(callCtx, g.retryAttempts, g.retryBackoff, func() (*pb.InstrumentResponse, error) {
return g.instrumentsPB.GetInstrumentBy(callCtx, &pb.InstrumentRequest{
IdType: pb.InstrumentIdType_INSTRUMENT_ID_TYPE_UID,
Id: instrumentUID,
})
})
})
if err != nil {
return 0, err
}
instrument := resp.GetInstrument()
if instrument == nil || instrument.GetLot() <= 0 {
return 0, fmt.Errorf("portfolio lot size is unavailable for %s", instrumentUID)
}
lot := int64(instrument.GetLot())
g.instrumentLots.Store(instrumentUID, lot)
return lot, nil
}
func portfolioDeprecatedQuantityLots(position *pb.PortfolioPosition) (decimal.Decimal, bool) {
message := position.ProtoReflect()
field := message.Descriptor().Fields().ByName("quantity_lots")