Skip to content

Conversation

@samuael
Copy link
Collaborator

@samuael samuael commented Jul 3, 2025

Shaloooooom 🕊️ ፐeam

This PR improves the gctcli user prompts for order submission, modification, and cancellation. Key updates include:

  • Enhanced CLI prompts based on newly updated input parameters.
  • Regeneration of the proto file and related Go code.
  • Updates to Submit, Modify, and Cancel structs for improved efficiency.
  • Adjustments to exchange logic reflecting struct changes.
  • Added updates and unit tests for order-related types such as PriceType and TrackingMode.

Fixes # (issue)

Type of change

Please delete options that are not relevant and add an x in [] as item is complete.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How has this been tested

Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration and
also consider improving test coverage whilst working on a certain feature or package.

  • go test ./... -race
  • golangci-lint run
  • Test X

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation and regenerated documentation via the documentation tool
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally and on Github Actions with my changes
  • Any dependent changes have been merged and published in downstream modules

Copy link
Collaborator

@shazbert shazbert left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a suggestion for everybody.

Name: "amount",
Usage: "the amount for the order",
},
&cli.StringFlag{
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before I do a full review I have one suggestion which will need to be agreed by everyone. I am having a real hard time justiying the amount of toil when doing stuff like this and also tracking arg position it just adds in so much bloat. Suggestion we use structs with tags to define cli flags which will cut down time on implementation. cli tags can be updated with required and usage flags in future. @thrasher- @gbjk

var submitOrderCommand = &cli.Command{
	Name:      "submitorder",
	Usage:     "submit order submits an exchange order",
	ArgsUsage: "<exchange> <pair> <side> <type> <amount> <price> <client_id>",
	Action:    submitOrder,
	Flags:     flagsFromStruct(submitOrderRequest),
}

type SubmitOrderParams struct {
	Exchange           string  `cli:"exchange"`
	Pair               string  `cli:"pair"`
	Side               string  `cli:"side"`
	Type               string  `cli:"type"`
	Amount             float64 `cli:"amount"`
	Asset              string  `cli:"asset"`
	Price              float64 `cli:"price"`
	Leverage           float64 `cli:"leverage"`
	ClientOrderID      string  `cli:"client_order_id"`
	MarginType         string  `cli:"margin_type"`
	TimeInForce        string  `cli:"time_in_force"`
	QuoteAmount        float64 `cli:"quote_amount"`
	ClientID           string  `cli:"client_id"`
	TriggerPrice       float64 `cli:"trigger_price"`
	TriggerLimitPrice  float64 `cli:"trigger_limit_price"`
	TriggerPriceType   string  `cli:"trigger_price_type"`
	TpPrice            float64 `cli:"tp_price"`
	TpLimitPrice       float64 `cli:"tp_limit_price"`
	TpPriceType        string  `cli:"tp_price_type"`
	SlPrice            float64 `cli:"sl_price"`
	SlLimitPrice       float64 `cli:"sl_limit_price"`
	SlPriceType        string  `cli:"sl_price_type"`
	TrackingMode       string  `cli:"tracking_mode"`
	TrackingValue      float64 `cli:"tracking_value"`
	Hidden             bool    `cli:"hidden"`
	Iceberg            bool    `cli:"iceberg"`
	AutoBorrow         bool    `cli:"auto_borrow"`
	ReduceOnly         bool    `cli:"reduce_only"`
	RetrieveFees       bool    `cli:"retrieve_fees"`
	RetrieveFeeDelayMs int64   `cli:"retrieve_fee_delay_ms"`
}

var submitOrderRequest = &SubmitOrderParams{}

func flagsFromStruct(params any) []cli.Flag {
	var flags []cli.Flag
	val := reflect.ValueOf(params).Elem()
	typ := val.Type()

	for i := 0; i < typ.NumField(); i++ {
		field := typ.Field(i)
		flagName := field.Tag.Get("cli")
		if flagName == "" {
			continue
		}

		switch field.Type.Kind() {
		case reflect.String:
			flags = append(flags, &cli.StringFlag{
				Name:  flagName,
				Usage: "the " + flagName + " for the order",
			})
		case reflect.Float64:
			flags = append(flags, &cli.Float64Flag{
				Name:  flagName,
				Usage: "the " + flagName + " for the order",
			})
		case reflect.Bool:
			flags = append(flags, &cli.BoolFlag{
				Name:  flagName,
				Usage: "the " + flagName + " for the order",
			})
		case reflect.Int64:
			flags = append(flags, &cli.Int64Flag{
				Name:  flagName,
				Usage: "the " + flagName + " for the order",
			})
		}
	}
	return flags
}

func submitOrder(c *cli.Context) error {
	if c.NArg() == 0 && c.NumFlags() == 0 {
		return cli.ShowSubcommandHelp(c)
	}

	assetType := strings.ToLower(submitOrderRequest.Asset)
	if !validAsset(assetType) {
		return errInvalidAsset
	}

	marginType := strings.ToLower(submitOrderRequest.MarginType)
	if !margin.IsValidString(marginType) {
		return margin.ErrInvalidMarginType
	}

	p, err := currency.NewPairDelimiter(submitOrderRequest.Pair, pairDelimiter)
	if err != nil {
		return err
	}

	conn, cancel, err := setupClient(c)
	if err != nil {
		return err
	}
	defer closeConn(conn, cancel)

	client := gctrpc.NewGoCryptoTraderServiceClient(conn)
	result, err := client.SubmitOrder(c.Context, &gctrpc.SubmitOrderRequest{
		Exchange: submitOrderRequest.Exchange,
		Pair: &gctrpc.CurrencyPair{
			Delimiter: p.Delimiter,
			Base:      p.Base.String(),
			Quote:     p.Quote.String(),
		},
		Amount:            submitOrderRequest.Amount,
		Price:             submitOrderRequest.Price,
		Leverage:          submitOrderRequest.Leverage,
		Side:              submitOrderRequest.Side,
		OrderType:         submitOrderRequest.Type,
		AssetType:         assetType,
		MarginType:        marginType,
		ClientId:          submitOrderRequest.ClientID,
		ClientOrderId:     submitOrderRequest.ClientOrderID,
		QuoteAmount:       submitOrderRequest.QuoteAmount,
		TimeInForce:       submitOrderRequest.TimeInForce,
		TriggerPrice:      submitOrderRequest.TriggerPrice,
		TriggerPriceType:  submitOrderRequest.TriggerPriceType,
		TriggerLimitPrice: submitOrderRequest.TriggerLimitPrice,
		StopLoss: &gctrpc.RiskManagement{
			Price:      submitOrderRequest.SlPrice,
			LimitPrice: submitOrderRequest.SlLimitPrice,
			PriceType:  submitOrderRequest.SlPriceType,
		},
		TakeProfit: &gctrpc.RiskManagement{
			Price:      submitOrderRequest.TpPrice,
			LimitPrice: submitOrderRequest.TpLimitPrice,
			PriceType:  submitOrderRequest.TpPriceType,
		},
		Hidden:             submitOrderRequest.Hidden,
		Iceberg:            submitOrderRequest.Iceberg,
		ReduceOnly:         submitOrderRequest.ReduceOnly,
		AutoBorrow:         submitOrderRequest.AutoBorrow,
		RetrieveFees:       submitOrderRequest.RetrieveFees,
		RetrieveFeeDelayMs: submitOrderRequest.RetrieveFeeDelayMs,
		TrackingMode:       submitOrderRequest.TrackingMode,
		TrackingValue:      submitOrderRequest.TrackingValue,
	})
	if err != nil {
		return err
	}

	jsonOutput(result)
	return nil
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forgot to set destination for flag can do something like this

Destination: reflect.ValueOf(params).Elem().FieldByName(field.Name).Addr().Interface().(*string), // For strings etc

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@thrasher- @gbjk There seems to be no prior agreement on direction here and this was pushed through, when you get some time could you quickly review?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had applied this change with different additions. Could you please look at it and let me know your thoughts?

@shazbert shazbert requested review from gbjk and thrasher- July 3, 2025 23:13
@shazbert shazbert added the review me This pull request is ready for review label Jul 3, 2025
@gbjk gbjk mentioned this pull request Jul 7, 2025
15 tasks
@samuael samuael requested a review from shazbert July 10, 2025 13:52
Copy link
Collaborator

@shazbert shazbert left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the changes, definately consolidates the code a bit, suggestions and requires a go mod tidy.

go.sum Outdated
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.0 h1:+epNPbD5EqgpEMm5wrl4Hqts3jZt8+kYaqUisuuIGTk=
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to run go mod tidy

}

// UnmarshalCLIFields unmarshals field values from command line as *cli.Context to an interface.
func UnmarshalCLIFields(c *cli.Context, params any) error {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: If you set a global you don't need to do this.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you clarify what you mean by this comment?
We want to move away from globals, don't we?

}

// FlagsFromStruct returns list of cli flags from exported flags
func FlagsFromStruct(params any, usageInfo map[string]string) []cli.Flag {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: can use specific tags for field.Tag.Get as name,usage, required for field and set destination for value, if no usage can set up a global map for default values if no name default to using the field name and try to standardise field names.

@samuael samuael requested a review from gbjk September 18, 2025 08:08
return errInvalidAsset
in = &gctrpc.WithdrawalEventsByExchangeRequest{
Exchange: arg.Exchange,
Limit: int32(arg.Limit), //nolint:gosec // TODO: SQL boiler's QueryMode limit only accepts the int type
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚧

  • Pretty sure we mean QueryMod
  • Confused why we say int type, but then give it int32. Especially since it accepts int.
  • Can you check if this would work with int? Seems like we're going through the client first anyway...
    🌴 Please make this change in similar comments below too
Suggested change
Limit: int32(arg.Limit), //nolint:gosec // TODO: SQL boiler's QueryMode limit only accepts the int type
Limit: int32(arg.Limit), //nolint:gosec // TODO: SQL boiler's QueryMod limit only accepts the int type

}

// UnmarshalCLIFields unmarshals field values from command line as *cli.Context to an interface.
func UnmarshalCLIFields(c *cli.Context, params any) error {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you clarify what you mean by this comment?
We want to move away from globals, don't we?

@samuael samuael requested a review from gbjk September 19, 2025 05:44
Copy link
Collaborator

@gbjk gbjk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for those changes

I've been considering the impact radius of our CLI processing here.
I've used viper to handle automatic flags and config files with merge-folding of defaults, user config, and test config from files, and then overlaying env vars and flags.
It doesn't need any scaffolding with tags, but it also doesn't handle requires or usage for you. OTOH, cli doesn't handle it for us either, so it's no worse off.
I guess the first question would be: Would the import list of viper be acceptable to even consider it. If it's not, then closed-case.
Sorry - If I was you I wouldn't want someone throwing this gasoline on my PR 🔥

Comment on lines +3338 to +3342
flagNames := strings.Split(field.Tag.Get("name"), ",")
if len(flagNames) == 0 || flagNames[0] == "" {
continue
}
flagName := flagNames[0]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚧 Looking at the amount of code we have to support them, and and the amount of definitions, could we just drop the name entirely, use the field name?
This assumes we can strip - out of params coming in to match them, which I'm expecting we could.
It feels like that'd make all the multiple name and shortened name complexity go away.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If what we need is to have a single name that would make sense, and I support that. However, the current implementation allows multiple alias names, and using a structure that supports this—such as the name tag—is the most appropriate approach.

By the way, Not all field Names and cli tag name are similar.

RetrieveFeeDelayMs int64 `name:"retrieve_fee_delay_ms"`
}

// GetOrderParams defines command-line flags for exchange order retrieval and unmarshal their values.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🐒 🌴 This concept applies throughout... exchange XYZ sounds weird

Suggested change
// GetOrderParams defines command-line flags for exchange order retrieval and unmarshal their values.
// GetOrderParams defines command-line flags for retrieving exchange orders

if !validPair(currencyPair) {
return fmt.Errorf("%w currencypair:%v", errInvalidPair, currencyPair)
if !validPair(arg.Pair) {
return fmt.Errorf("%w currencypair:%v", errInvalidPair, arg.Pair)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return fmt.Errorf("%w currencypair:%v", errInvalidPair, arg.Pair)
return fmt.Errorf("%w: %q", errInvalidPair, arg.Pair)

if !validPair(currencyPair) {
return fmt.Errorf("%w currencypair:%v", errInvalidPair, currencyPair)
if !validPair(arg.Pair) {
return fmt.Errorf("%w currencypair:%v", errInvalidPair, arg.Pair)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚧

Suggested change
return fmt.Errorf("%w currencypair:%v", errInvalidPair, arg.Pair)
return fmt.Errorf("%w: %q", errInvalidPair, arg.Pair)

if !validPair(currencyPair) {
return fmt.Errorf("%w currencypair:%v", errInvalidPair, currencyPair)
if !validPair(arg.Pair) {
return fmt.Errorf("%w currencypair:%v", errInvalidPair, arg.Pair)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚧

Suggested change
return fmt.Errorf("%w currencypair:%v", errInvalidPair, arg.Pair)
return fmt.Errorf("%w: %q", errInvalidPair, arg.Pair)

if !margin.IsValidString(marginType) {
return fmt.Errorf("%w margintype:%v", margin.ErrInvalidMarginType, marginType)
if !margin.IsValidString(arg.MarginType) {
return fmt.Errorf("%w margintype:%v", margin.ErrInvalidMarginType, arg.MarginType)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚧

Suggested change
return fmt.Errorf("%w margintype:%v", margin.ErrInvalidMarginType, arg.MarginType)
return fmt.Errorf("%w: %q", margin.ErrInvalidMarginType, arg.MarginType)

return fmt.Errorf("%w currencypair:%v", errInvalidPair, currencyPair)
if arg.Pair != "" {
if !validPair(arg.Pair) {
return fmt.Errorf("%w currencypair:%v", errInvalidPair, arg.Pair)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚧

Suggested change
return fmt.Errorf("%w currencypair:%v", errInvalidPair, arg.Pair)
return fmt.Errorf("%w: %q", errInvalidPair, arg.Pair)

@shazbert shazbert added reconstructing Based on PR feedback, this is currently being reworked and is not to be merged and removed review me This pull request is ready for review labels Sep 23, 2025
@shazbert shazbert added review me This pull request is ready for review and removed reconstructing Based on PR feedback, this is currently being reworked and is not to be merged labels Oct 9, 2025
@samuael samuael requested a review from gbjk November 16, 2025 08:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review me This pull request is ready for review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants