Daily Holding-Cost Models with asharechips

Ruihong Zhao

What a cost distribution describes

A distribution assigns a share of circulating stock to each modelled holding cost. Average cost is weighted by shares, rather than by the number of distinct price levels:

chip_mean_cost(c(10, 11, 12), c(50, 20, 30))
#> [1] 10.8

Actual holders’ acquisition costs are not available in ordinary daily market data. This package instead specifies the assumptions used to carry old weights forward and allocate new weights.

A small analytical example

The following data are entirely synthetic, not a real stock history. Each day has a single transaction price. Dates are deliberately out of order.

daily <- data.frame(
  trade_date = c("20260106", "20260102", "20260105"),
  high = c(12, 10, 11), low = c(12, 10, 11), close = c(12, 10, 11),
  vol = c(100, 100, 100), amount = c(120, 100, 110),
  turnover_rate = c(30, 0, 20)
)
chips <- estimate_chip_distribution(daily)
as.data.frame(chips)
#>   price    weight  percent
#> 1    10 0.6065307 60.65307
#> 2    11 0.1342876 13.42876
#> 3    12 0.2591818 25.91818
summary(chips)
#>   ts_code start_date end_date observations last_close average_cost median_cost
#> 1    <NA>   20260102 20260106            3         12     10.65265          10
#>   peak_price peak_percent profit_ratio profit_percent breakeven_ratio
#> 1         10     60.65307    0.7408182       74.08182       0.2591818
#>   loss_ratio cost_low_90 cost_high_90 concentration_90
#> 1          0          10           12       0.09090909
#>   cumulative_turnover_percent history_coverage initialization_weight
#> 1                          50        0.3934693             0.6065307

Tushare reports volume in lots of 100 shares and amount in thousand CNY, so VWAP is amount * 10 / vol. See the daily-data documentation and turnover definitions.

The first row initializes the distribution at 10 CNY. After turnover of 20 and 30 percentage points on the next two days, the analytical weights are:

expected <- c(
  "10 CNY" = exp(-0.5),
  "11 CNY" = (1 - exp(-0.2)) * exp(-0.3),
  "12 CNY" = 1 - exp(-0.3)
)
expected
#>    10 CNY    11 CNY    12 CNY 
#> 0.6065307 0.1342876 0.2591818
stopifnot(isTRUE(all.equal(unname(expected), chips$distribution$weight)))

At a final close of 12, the 12-CNY weight is break-even; it is not counted as profitable. The three profit/break-even/loss ratios sum to one.

plot(chips, main = "Synthetic daily cost distribution")

Model choices and initialization sensitivity

The exponential rule retains exp(-turnover_rate / 100) of old weight each day. It represents homogeneous random replacement. Linear replacement uses turnover directly, is easier to interpret as a single transfer, and requires rates at most 100 percentage points. They agree approximately for very small turnover.

The linear update builds on the turnover-based cost-distribution recurrence of Liu and Wang (2018). The exponential retention transform and each daily price-allocation rule are explicit package-specific modelling choices.

linear <- estimate_chip_distribution(daily, turnover_transform = "linear")
as.data.frame(linear)
#>   price weight percent
#> 1    10   0.56      56
#> 2    11   0.14      14
#> 3    12   0.30      30

The default initialization is a strong assumption. A short interval with little turnover can retain most of the first day’s assigned weight. The diagnostics distinguish proposed turnover from the replacement actually applied to that initialized first row:

chips$diagnostics[c("trade_date", "replacement_fraction",
                    "applied_replacement_fraction", "initialized_here")]
#>   trade_date replacement_fraction applied_replacement_fraction initialized_here
#> 1   20260102            0.0000000                    0.0000000             TRUE
#> 2   20260105            0.1812692                    0.1812692            FALSE
#> 3   20260106            0.2591818                    0.2591818            FALSE
chips$summary[c("initialization_weight", "history_coverage")]
#>   initialization_weight history_coverage
#> 1             0.6065307        0.3934693

If a distribution immediately before the first date is available, supply it in the final observation’s price basis. Every row then applies turnover:

initial <- data.frame(price = c(8, 12), weight = c(0.5, 0.5))
with_initial <- estimate_chip_distribution(daily, initial_distribution = initial)
summary(with_initial)
#>   ts_code start_date end_date observations last_close average_cost median_cost
#> 1    <NA>   20260102 20260106            3         12     10.65265          12
#>   peak_price peak_percent profit_ratio profit_percent breakeven_ratio
#> 1         12     56.24471    0.4375529       43.75529       0.5624471
#>   loss_ratio cost_low_90 cost_high_90 concentration_90
#> 1          0           8           12              0.2
#>   cumulative_turnover_percent history_coverage initialization_weight
#> 1                          50        0.3934693             0.6065307

Price allocation and resolution

The VWAP method places new weight at a single daily average price. Typical-price and close methods offer alternatives when volume and amount are unavailable. The uniform rule allocates weight by overlaps of grid cells with the daily low-high range. Daily data do not prove that trades were uniformly distributed.

range_data <- data.frame(
  trade_date = "20260102", high = 12, low = 10, close = 11, turnover_rate = 0
)
uniform <- estimate_chip_distribution(range_data, price_method = "uniform", price_step = 0.25)
plot(uniform, main = "Uniform allocation is a modelling assumption")

Smaller price_step reduces rounding error but increases computation. Cost peaks and threshold-based profit shares can change with resolution. The nominal central 90-percent interval consists of discrete 5th and 95th weighted quantiles; tied mass at endpoints can make its contained weight exceed 90 percent.

Joining already downloaded Tushare tables

The package never downloads data or saves credentials. The following joined tables are also synthetic:

raw_daily <- daily[setdiff(names(daily), "turnover_rate")]
raw_daily$ts_code <- "000001.SZ"
basic <- data.frame(ts_code = "000001.SZ", trade_date = daily$trade_date,
                    turnover_rate = daily$turnover_rate)
factors <- data.frame(ts_code = "000001.SZ", trade_date = daily$trade_date,
                      adj_factor = 1)
input <- prepare_tushare_data(raw_daily, basic, factors, ts_code = "000001.SZ")
estimate_chip_distribution(input, adjustment_col = "adj_factor")
#> <chip_distribution>
#>   Stock: 000001.SZ
#>   Period: 20260102 to 20260106 (3 rows)
#>   Average cost: 10.6527 CNY
#>   Last close: 12 CNY
#>   Estimated profitable chips: 74.08%
#>   Initialization weight remaining: 60.65%

Duplicate keys or absent matches are errors. A cutoff can be supplied through as_of, including weekends; the last retained daily observation determines the factor anchor. When factors are supplied, historical prices are multiplied by factor_day / factor_last. Supply unadjusted daily prices to avoid adjusting twice. Dividend adjustment creates a comparable price basis rather than literal historical cash acquisition costs.

Interpreting limitations

The model cannot identify which cost layers were sold, intraday trade-price patterns, locked holdings, investor identities, or the costs of new shares. Issuance, unlocks and repurchases are not modelled. If a float_share column is present, consecutive changes are flagged and produce a warning. Fees and taxes are excluded. Estimates and sensitivity comparisons describe model behaviour; they do not predict future returns.

For field definitions and validation rules, see ?estimate_chip_distribution. Chinese guidance is available at:

system.file("README.zh-CN.md", package = "asharechips")
#> [1] "/private/var/folders/4m/4fmpnlcn1vxg6btldtgs420m0000gn/T/Rtmp81fJNB/Rinst9382034e8a9/asharechips/README.zh-CN.md"