n P

时间序列信用卡消费策略

Time series credit cards payment

Posted by Zeusro on December 11, 2025
👈🏻 Select language

微信支付的策略是基于最后一笔,而支付宝的支付顺序总是瞎搞,无论是自动顺序支付和系统自动匹配,带着明显的平台倾向性。这些都不是我想要的结果。

我决定自己设计一个时间序列信用卡消费策略,完成每一笔信用卡支付。

形式逻辑和定义

時間序列:是一組按照時間發生先後順序進行排列的數據點序列。

时间序列对象:基于时间序列的程序语言对象。时间必须是第一成员。

例如:

1
2
3
4
5
6
type Deal struct {
	t       time.Time
	Money   float32
	policy  string
	Payment Card
}

时间序列函数:基于时间序列的程序语言函数。时间(或者时间序列对象)必须是第一参数成员。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func (p *DiscountPolicys) MVP(d Deal) DiscountPolicy {
	var best DiscountPolicy
	var maxDiscount float32 = 0
	for _, policy := range p.Resource {
		if !policy.Match(d) {
			continue
		}
		if policy.Discount >= maxDiscount {
			continue
		}
		best = policy
		if best.N > 0 {
			maxDiscount = best.Discount
			//资源池减一
			best.N--
		}
	}
	if len(best.Bank) > 0 {
		p.Resource[best.Bank] = best
	}
	return best
}

// Cards 从本地文件加载信用卡信息,格式是每行 "银行 账单日/最后还款日"。如“农业银行 1/26”
func (z Zeusro) Cards(t time.Time) []Card {
    //省略
}

时间序列不动点:信用卡账单日的第二天。

账单日第二天消费策略:在信用卡账单日的第二天信用卡消费策略。

最大交易折扣交易策略:每一笔信用卡支付选取折扣金额最大的交易途径的消费策略。

基本类型建模

信用卡、交易、交易策略。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type Card struct {
	Bank        string    //金融机构
	BillingDate time.Time //账单日
	LastDate    time.Time //最后还款日
}

type Deal struct {
	t       time.Time
	Money   float32
	policy  string
	Payment Card
}

type Policy interface {
	Match(d Deal) bool
	Name() string
	MVP(d Deal) Policy
}

基于交易金额的二分算法

信用卡消费是一种延长应付账款的消费。单次交易的交易费用(1%左右)由商家承担。 而从消费者角度考虑,申请多张信用卡,并设置不同的账单日,就相当于循环账期,形成个人的“永续债”。

小额消费主要以薅银行羊毛为主。比如农业银行的18-0.3,浦发银行的16-0.01,广发银行的n-0.01。个人感受是1000元以上的消费,以账单日第二天消费为宜——即尽量在信用卡账单日的第二天消费,为最佳时序不动点。

因此总的入口算法,以消费金额,使用二分法消费:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func (z Zeusro) Pay(deals []Deal) []Deal {
	//follow the rule and find the best policy
	discount := NewDiscountPolicys(DefaultDiscountPolicys)
	for i, deal := range deals {
		if deal.Money > SmallMoney {
			//账单日第二天消费策略 BillingDatePolicy
			policy1 := NewBillingDatePolicy(z.Cards(deal.t))
			mvp := policy1.MVP(deal)
			best := mvp.BestCard
			deals[i].Payment = best
			deals[i].policy = policy1.Name()
		} else {
			//最大交易折扣交易策略 DiscountPolicys
			mvp := discount.MVP(deal)
			card := Card{Bank: mvp.Bank}
			deals[i].Payment = card
			deals[i].policy = mvp.Name()
		}
	}
	return deals
}

账单日第二天消费策略

基本交易倾向是遍历所有信用卡,尽量接近账单第二日交易。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func (p BillingDatePolicy) MVP(d Deal) BillingDatePolicy {
	if len(p.cards) == 0 {
		return p
	}

	now := d.t
	var bestCard *Card
	var bestDelta time.Duration = time.Hour * 24 * 31 // 极大值

	for i := range p.cards {
		c := &p.cards[i]
		delta := now.Sub(c.BillingDate.AddDate(0, 0, 1))
		if delta > 0 && delta < bestDelta {
			bestDelta = delta
			bestCard = c
		}
	}
	if bestCard != nil {
		p.BestCard = *bestCard
	}
	return p
}

最大交易折扣交易策略

也叫薅羊毛算法。这个算法很简单,就是哪里有钱去哪里。以农业银行来说,参加每月活动报名之后,日消费第一笔18元减0.3元,浦发银行需要16元,以此类推。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func (p *DiscountPolicys) MVP(d Deal) DiscountPolicy {
	var best DiscountPolicy
	var maxDiscount float32 = 0
	for _, policy := range p.Resource {
		if !policy.Match(d) {
			continue
		}
		if policy.Discount >= maxDiscount {
			continue
		}
		best = policy
		if best.N > 0 {
			maxDiscount = best.Discount
			//资源池减一
			best.N--
		}
	}
	if len(best.Bank) > 0 {
		p.Resource[best.Bank] = best
	}
	return best
}

完整算法在我的自己实现的 OOOS 里面

每天0.3元,我要薅到农行倒闭

img

WeChat Pay’s strategy is based on the last transaction, while Alipay’s payment order is always chaotic. Whether it is automatic payment order or system auto‑matching, the platform bias is obvious. None of these behaviors are what I want.

I decided to design my own time‑series credit‑card consumption strategy to control every single credit‑card payment.

Formal Logic and Definitions

Time series: a sequence of data points arranged according to the order in which they occur over time.

Time‑series object: a programming‑language object based on a time series. Time must be the first member.

Example:

1
2
3
4
5
6
type Deal struct {
    t       time.Time
    Money   float32
    policy  string
    Payment Card
}

Time‑series function: a programming‑language function based on a time series. Time (or a time‑series object) must be the first parameter.

Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func (p *DiscountPolicys) MVP(d Deal) DiscountPolicy {
    var best DiscountPolicy
    var maxDiscount float32 = 0
    for _, policy := range p.Resource {
        if !policy.Match(d) {
            continue
        }
        if policy.Discount >= maxDiscount {
            continue
        }
        best = policy
        if best.N > 0 {
            maxDiscount = best.Discount
            // reduce resource pool
            best.N--
        }
    }
    if len(best.Bank) > 0 {
        p.Resource[best.Bank] = best
    }
    return best
}

// Cards loads credit‑card information from a local file.
// Format: each line is "Bank BillingDate/LastDate".
// Example: "AgriculturalBank 1/26"
func (z Zeusro) Cards(t time.Time) []Card {
    // omitted
}

Time‑series fixed point: the second day after a credit card’s billing date.

Billing‑date‑plus‑one strategy: a credit‑card spending strategy that executes on the day after the billing date.

Maximum‑discount transaction strategy: for each transaction, choose the payment method that yields the highest discount.

Basic Type Modeling

Credit cards, transactions, and transaction strategies.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type Card struct {
    Bank        string    // financial institution
    BillingDate time.Time // billing date
    LastDate    time.Time // last repayment date
}

type Deal struct {
    t       time.Time
    Money   float32
    policy  string
    Payment Card
}

type Policy interface {
    Match(d Deal) bool
    Name() string
    MVP(d Deal) Policy
}

Binary‑Search Algorithm Based on Transaction Amount

Credit‑card spending is a way to extend accounts payable. The transaction fee (around 1%) is paid by the merchant.

From the consumer’s perspective, applying for multiple credit cards with different billing dates effectively forms a rotating credit cycle — a kind of personal “perpetual bond.”

Small transactions are mainly used to farm bank rewards. For example, Agricultural Bank offers 18 yuan − 0.3 yuan for the day’s first payment after activation; SPD Bank has 16 yuan − 0.01 yuan; GF Bank has n − 0.01 yuan, etc.

Based on personal experience, for payments over 1000 yuan, the best timing is the second day after the billing date — the optimal time‑series fixed point.

Therefore, the top‑level algorithm uses a binary decision on the transaction amount:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func (z Zeusro) Pay(deals []Deal) []Deal {
    // follow the rule and find the best policy
    discount := NewDiscountPolicys(DefaultDiscountPolicys)
    for i, deal := range deals {
        if deal.Money > SmallMoney {
            // BillingDatePolicy: spend on the day after billing date
            policy1 := NewBillingDatePolicy(z.Cards(deal.t))
            mvp := policy1.MVP(deal)
            best := mvp.BestCard
            deals[i].Payment = best
            deals[i].policy = policy1.Name()
        } else {
            // Maximum discount strategy: go wherever the discount is highest
            mvp := discount.MVP(deal)
            card := Card{Bank: mvp.Bank}
            deals[i].Payment = card
            deals[i].policy = mvp.Name()
        }
    }
    return deals
}

Billing‑Date‑Plus‑One Strategy

The fundamental tendency is to iterate over all credit cards and choose the one whose billing‑date‑plus‑one is closest.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func (p BillingDatePolicy) MVP(d Deal) BillingDatePolicy {
    if len(p.cards) == 0 {
        return p
    }

    now := d.t
    var bestCard *Card
    var bestDelta time.Duration = time.Hour * 24 * 31 // large initial value

    for i := range p.cards {
        c := &p.cards[i]
        delta := now.Sub(c.BillingDate.AddDate(0, 0, 1))
        if delta > 0 && delta < bestDelta {
            bestDelta = delta
            bestCard = c
        }
    }
    if bestCard != nil {
        p.BestCard = *bestCard
    }
    return p
}

Maximum‑Discount Transaction Strategy

Also called the “reward‑farming algorithm.” It is simple: follow the discount.

For example, with Agricultural Bank, after registering for the monthly activity, the first daily consumption of 18 yuan gives a 0.3 yuan discount; SPD Bank requires 16 yuan; GF Bank uses n − 0.01, and so on.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func (p *DiscountPolicys) MVP(d Deal) DiscountPolicy {
    var best DiscountPolicy
    var maxDiscount float32 = 0
    for _, policy := range p.Resource {
        if !policy.Match(d) {
            continue
        }
        if policy.Discount >= maxDiscount {
            continue
        }
        best = policy
        if best.N > 0 {
            maxDiscount = best.Discount
            // reduce resource pool
            best.N--
        }
    }
    if len(best.Bank) > 0 {
        p.Resource[best.Bank] = best
    }
    return best
}

The complete algorithm is in my self‑implemented OOOS.

0.3 Yuan per Day — I Will Farm Until Agricultural Bank Collapses

img

Стратегия WeChat Pay основана на «последней транзакции», тогда как порядок платежей Alipay всегда хаотичен. Автоматическое списание и системное сопоставление демонстрируют явный платформенный перекос. Всё это не соответствует тому, что мне нужно.

Поэтому я решил разработать собственную стратегию расходования кредитных карт на основе временных рядов, чтобы контролировать каждую операцию.

Формальная логика и определения

Временной ряд — последовательность точек данных, упорядоченных по времени.

Объект временного ряда — объект языка программирования, основанный на временном ряде. Время обязательно является первым полем.

Пример:

1
2
3
4
5
6
type Deal struct {
	t       time.Time
	Money   float32
	policy  string
	Payment Card
}

Функция временного ряда — это функция, основанная на временных данных. Параметр времени (или объект временного ряда) должен быть первым параметром.

Например:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func (p *DiscountPolicys) MVP(d Deal) DiscountPolicy {
	var best DiscountPolicy
	var maxDiscount float32 = 0
	for _, policy := range p.Resource {
		if !policy.Match(d) {
			continue
		}
		if policy.Discount >= maxDiscount {
			continue
		}
		best = policy
		if best.N > 0 {
			maxDiscount = best.Discount
			// уменьшение ресурсного пула
			best.N--
		}
	}
	if len(best.Bank) > 0 {
		p.Resource[best.Bank] = best
	}
	return best
}

// Cards загружает информацию о кредитных картах из локального файла.
// Формат: каждая строка — «банк дата_биллинга/дата_погашения».
// Например: «ABC 1/26»
func (z Zeusro) Cards(t time.Time) []Card {
    // опущено
}

Неподвижная точка временного ряда — второй день после даты выписки кредитной карты.

Стратегия расходования на второй день после даты выписки — стратегия использования кредитных карт на следующий день после даты выписки.

Стратегия максимальной скидки — для каждой операции выбирается способ оплаты с наибольшей скидкой.

Моделирование базовых типов

Кредитные карты, транзакции, стратегии транзакций.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type Card struct {
	Bank        string    // финансовая организация
	BillingDate time.Time // дата выписки
	LastDate    time.Time // крайний срок погашения
}

type Deal struct {
	t       time.Time
	Money   float32
	policy  string
	Payment Card
}

type Policy interface {
	Match(d Deal) bool
	Name() string
	MVP(d Deal) Policy
}

Бинарный алгоритм на основе суммы транзакции

Расходование по кредитной карте — это форма продления кредиторской задолженности. Комиссия за транзакцию (около 1%) оплачивается продавцом.

С точки зрения потребителя, оформление нескольких кредитных карт с разными датами выписки создаёт цикл вращающихся долгов — персональный «вечный долг» (perpetual bond).

Мелкие покупки в основном используются для получения бонусов банков. Например:

  • ABC: 18 – 0.3
  • SPDB: 16 – 0.01
  • CGB: n – 0.01

По моему опыту, для расходов более 1000 юаней оптимально платить на второй день после даты выписки — это оптимальная неподвижная точка временного ряда.

Поэтому общий алгоритм определяет стратегию на основе бинарного разбиения суммы:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func (z Zeusro) Pay(deals []Deal) []Deal {
	// follow the rule and find the best policy
	discount := NewDiscountPolicys(DefaultDiscountPolicys)
	for i, deal := range deals {
		if deal.Money > SmallMoney {
			// стратегия расходования на второй день после биллинга
			policy1 := NewBillingDatePolicy(z.Cards(deal.t))
			mvp := policy1.MVP(deal)
			best := mvp.BestCard
			deals[i].Payment = best
			deals[i].policy = policy1.Name()
		} else {
			// стратегия максимальной скидки
			mvp := discount.MVP(deal)
			card := Card{Bank: mvp.Bank}
			deals[i].Payment = card
			deals[i].policy = mvp.Name()
		}
	}
	return deals
}

Стратегия расходования на второй день после даты выписки

Основной принцип — перебрать все кредитные карты и выбрать ту, чья дата «выписка + 1 день» наиболее близка к текущему времени.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func (p BillingDatePolicy) MVP(d Deal) BillingDatePolicy {
	if len(p.cards) == 0 {
		return p
	}

	now := d.t
	var bestCard *Card
	var bestDelta time.Duration = time.Hour * 24 * 31 // большое значение

	for i := range p.cards {
		c := &p.cards[i]
		delta := now.Sub(c.BillingDate.AddDate(0, 0, 1))
		if delta > 0 && delta < bestDelta {
			bestDelta = delta
			bestCard = c
		}
	}
	if bestCard != nil {
		p.BestCard = *bestCard
	}
	return p
}

Стратегия максимальной скидки

Называется также алгоритмом «снятия сливок» — идти туда, где дают выгоду.

Например, в ABC после регистрации в ежемесячной акции первая ежедневная операция на 18 юаней даёт скидку 0.3 юаня.
SPDB — 16 юаней.
И так далее.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func (p *DiscountPolicys) MVP(d Deal) DiscountPolicy {
	var best DiscountPolicy
	var maxDiscount float32 = 0
	for _, policy := range p.Resource {
		if !policy.Match(d) {
			continue
		}
		if policy.Discount >= maxDiscount {
			continue
		}
		best = policy
		if best.N > 0 {
			maxDiscount = best.Discount
			// уменьшение ресурсного пула
			best.N--
		}
	}
	if len(best.Bank) > 0 {
		p.Resource[best.Bank] = best
	}
	return best
}

Полный алгоритм находится в моём собственном проекте OOOS

Каждый день 0.3 юаня — буду получать, пока ABC не развалится

img