Mirror of https://github.com/roostorg/coop
github.com/roostorg/coop
1import sequelize, {
2 type HasOneGetAssociationMixin,
3 type InferAttributes,
4 type InferCreationAttributes,
5 type NonAttribute,
6 type Sequelize,
7} from 'sequelize';
8
9import { type DataTypes } from '../index.js';
10import { type User } from '../UserModel.js';
11import { type Rule } from './RuleModel.js';
12
13const { Model } = sequelize;
14
15export type Backtest = InstanceType<ReturnType<typeof makeBacktestModel>>;
16export enum BacktestStatus {
17 RUNNING = 'RUNNING',
18 COMPLETE = 'COMPLETE',
19 CANCELED = 'CANCELED',
20}
21
22const makeBacktestModel = (sequelize: Sequelize, DataTypes: DataTypes) => {
23 class Backtest extends Model<
24 InferAttributes<Backtest>,
25 InferCreationAttributes<
26 Backtest,
27 // fields that _cannot be set explicitly_ at creation time, b/c they have
28 // db defaults that must always apply (i.e., cannot be overriden) at creation.
29 {
30 omit:
31 | 'contentItemsProcessed'
32 | 'contentItemsMatched'
33 | 'status'
34 | 'createdAt'
35 | 'updatedAt'
36 | 'samplingComplete'
37 | 'sampleActualSize'
38 | 'cancelationDate';
39 }
40 >
41 > {
42 public declare id: string;
43
44 public declare ruleId: string;
45 public declare rule?: Rule;
46
47 public declare creatorId: string;
48 public declare creator?: User;
49
50 public declare sampleDesiredSize: number;
51 public declare sampleActualSize: number;
52 public declare sampleStartAt: Date;
53 public declare sampleEndAt: Date;
54
55 public declare cancelationDate: Date;
56 public declare samplingComplete: boolean;
57
58 public declare contentItemsProcessed: number;
59 public declare contentItemsMatched: number;
60
61 public declare status: BacktestStatus;
62
63 public declare createdAt: Date;
64 public declare updatedAt: Date;
65
66 declare getRule: HasOneGetAssociationMixin<Rule>;
67 declare getCreator: HasOneGetAssociationMixin<User>;
68
69 // eslint-disable-next-line @typescript-eslint/no-explicit-any
70 static associate(models: { [key: string]: any }) {
71 Backtest.belongsTo(models.User, { as: 'Creator' });
72 Backtest.belongsTo(models.Rule, { as: 'Rule' });
73 }
74
75 public static async hasRunningBacktestsForRule(ruleId: string) {
76 // ugh the built-in sequelize query builder sucks.
77 // https://github.com/sequelize/sequelize/issues/10187
78 return Backtest.findOne({
79 where: { ruleId, status: BacktestStatus.RUNNING },
80 }).then((it) => it != null);
81 }
82
83 public static async cancelRunningBacktestsForRule(ruleId: string) {
84 return Backtest.update(
85 { cancelationDate: new Date() },
86 { where: { ruleId, status: BacktestStatus.RUNNING } },
87 );
88 }
89
90 public async cancel() {
91 this.cancelationDate = new Date();
92 await this.save();
93 return this;
94 }
95
96 /**
97 * Because our queues will deliver sampled content items to be processed
98 * _at least once_, it’s possible that, rarely, contentItemsProcessed will
99 * be greater than sampleActualSize. To mitigate this, we clamp the
100 * exposed value for contentItemsProcessed at sampleActualSize.
101 */
102 public get correctedContentItemsProcessed(): NonAttribute<number> {
103 return Math.min(this.sampleActualSize, this.contentItemsProcessed);
104 }
105
106 /**
107 * Similar to {@see correctedContentItemsProcessed}, we clamp the exposed
108 * value of contentItemsMatched, since we can't logically have matched more
109 * items than we processed.
110 */
111 public get correctedContentItemsMatched(): NonAttribute<number> {
112 return Math.min(
113 this.correctedContentItemsProcessed,
114 this.contentItemsMatched,
115 );
116 }
117 }
118
119 /* Fields */
120 Backtest.init(
121 {
122 id: { type: DataTypes.STRING, primaryKey: true },
123 ruleId: { type: DataTypes.STRING, allowNull: false },
124 creatorId: { type: DataTypes.STRING, allowNull: false },
125
126 sampleDesiredSize: { type: DataTypes.INTEGER, allowNull: false },
127 sampleActualSize: {
128 type: DataTypes.INTEGER,
129 allowNull: false,
130 defaultValue: 0,
131 },
132 sampleStartAt: { type: DataTypes.DATE, allowNull: false },
133 sampleEndAt: { type: DataTypes.DATE, allowNull: false },
134
135 cancelationDate: { type: DataTypes.DATE },
136 samplingComplete: {
137 type: DataTypes.BOOLEAN,
138 allowNull: false,
139 defaultValue: false,
140 },
141
142 contentItemsProcessed: {
143 type: DataTypes.INTEGER,
144 allowNull: false,
145 defaultValue: 0,
146 },
147 contentItemsMatched: {
148 type: DataTypes.INTEGER,
149 allowNull: false,
150 defaultValue: 0,
151 },
152
153 status: { type: DataTypes.STRING },
154 createdAt: { type: DataTypes.DATE, allowNull: false },
155 updatedAt: { type: DataTypes.DATE, allowNull: false },
156 },
157 {
158 sequelize,
159 modelName: 'backtest',
160 underscored: true,
161 timestamps: true,
162 },
163 );
164
165 return Backtest;
166};
167
168export default makeBacktestModel;