forked from Giveth/coodcad
-
Notifications
You must be signed in to change notification settings - Fork 0
/
entities.py
220 lines (182 loc) · 8.39 KB
/
entities.py
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
import random
import uuid
from enum import Enum
from inspect import getmembers
from os.path import abspath
from types import FunctionType
from typing import List, Tuple
import numpy as np
import config
from convictionvoting import trigger_threshold
from hatch import TokenBatch
from utils import probability
"""
Helper functions from
https://stackoverflow.com/questions/192109/is-there-a-built-in-function-to-print-all-the-current-properties-and-values-of-a
to print all attributes of a class without me explicitly coding it out.
"""
def api(obj):
return [name for name in dir(obj) if name[0] != '_']
def attrs(obj):
disallowed_properties = {
name for name, value in getmembers(type(obj))
if isinstance(value, (property, FunctionType))}
return {
name: getattr(obj, name) for name in api(obj)
if name not in disallowed_properties and hasattr(obj, name)}
ProposalStatus = Enum("ProposalStatus", "CANDIDATE ACTIVE COMPLETED FAILED")
# candidate: proposal is being evaluated by the commons
# active: has been approved and is funded
# completed: the proposal was effective/successful
# failed: did not get to active status or failed after funding
class Proposal:
def __init__(self, funds_requested: int, trigger: float):
self.uuid = uuid.uuid4()
self.conviction = 0
self.status = ProposalStatus.CANDIDATE
self.age = 0
self.funds_requested = funds_requested
self.trigger = trigger
def __repr__(self):
return "<{} {}>".format(self.__class__.__name__, attrs(self))
def update_age(self):
self.age += 1
return self.age
def update_threshold(self, funding_pool: float, token_supply: float):
if self.status == ProposalStatus.CANDIDATE:
self.trigger = trigger_threshold(
self.funds_requested, funding_pool, token_supply)
else:
self.trigger = np.nan
return self.trigger
def has_enough_conviction(self, funding_pool: float, token_supply: float):
"""
It's just a conviction < threshold check, but we recalculate the
trigger_threshold so that the programmer doesn't have to remember to run
update_threshold before running this method.
"""
if self.status == ProposalStatus.CANDIDATE:
threshold = trigger_threshold(
self.funds_requested, funding_pool, token_supply)
if self.conviction < threshold:
return False
return True
else:
raise(Exception(
"Proposal {} is not a Candidate Proposal and so asking it if it will pass is inappropriate".format(str(self.uuid))))
class Participant:
def __init__(self, holdings_vesting: TokenBatch = None, holdings_nonvesting: TokenBatch = None):
self.name = "Somebody"
self.sentiment = np.random.rand()
self.holdings_vesting = holdings_vesting
self.holdings_nonvesting = holdings_nonvesting
def __repr__(self):
return "<{} {}>".format(self.__class__.__name__, attrs(self))
def buy(self) -> float:
"""
If the Participant decides to buy more tokens, returns the number of
tokens. Otherwise, return 0.
This method does not modify itself, it simply returns the answer so that
cadCAD's state update functions will make the changes and maintain its
functional-ness.
"""
engagement_rate = 0.3 * self.sentiment
force = self.sentiment - config.sentiment_sensitivity
if probability(engagement_rate) and force > 0:
delta_holdings = np.random.rand() * force
return delta_holdings
return 0
def sell(self) -> float:
"""
Decides to sell some tokens, and if so how many. If the Participant
decides to sell some tokens, returns the number of tokens. Otherwise,
return 0.
This method does not modify itself, it simply returns the answer so that
cadCAD's state update functions will make the changes and maintain its
functional-ness.
"""
engagement_rate = 0.3 * self.sentiment
force = self.sentiment - config.sentiment_sensitivity
if probability(engagement_rate) and force < 0:
delta_holdings = np.random.rand() * force
return delta_holdings
return 0
def create_proposal(self, total_funds_requested, median_affinity, funding_pool) -> bool:
"""
Here the Participant will decide whether or not to create a new
Proposal.
This equation, originally from randomly_gen_new_proposal(), is a
systems-type simulation. An individual Participant would likely think in
a different way, and thus this equation should change. Nevertheless for
simplicity's sake, we use this same equation for now.
Explanation: If the median affinity is high, the Proposal Rate should be
high.
If total funds_requested in candidate proposals is much lower than the
funding pool (i.e. the Commons has lots of spare money), then people are
just going to pour in more Proposals.
"""
percent_of_funding_pool_being_requested = total_funds_requested/funding_pool
proposal_rate = median_affinity / \
(1 + percent_of_funding_pool_being_requested)
new_proposal = probability(proposal_rate)
return new_proposal
def vote_on_candidate_proposals(self, candidate_proposals: dict) -> dict:
"""
Here the Participant decides which Candidate Proposals he will stake
tokens on. This method does not decide how many tokens he will stake
on them, because another function should decide how the tokens should be
balanced across the newly supported proposals and the ones the
Participant already supported.
Copied from
participants_buy_more_if_they_feel_good_and_vote_for_proposals()
candidate dict format:
{
"proposalUUID": affinity,
...
}
NOTE: the original cadCAD policy returned {'delta_holdings':
delta_holdings, 'proposals_supported': proposals_supported}
proposals_supported seems to include proposals ALREADY supported by the
participant, but I don't think it is needed.
"""
new_voted_proposals = {}
engagement_rate = .3*self.sentiment
if probability(engagement_rate):
# Put your tokens on your favourite Proposals, where favourite is
# calculated as 0.75 * (the affinity for the Proposal you like the
# most) e.g. if there are 2 Proposals that you have affinity 0.8,
# 0.9, then 0.75*0.9 = 0.675, so you will end up voting for both of
# these Proposals
#
# A Zargham work of art.
for candidate in candidate_proposals:
affinity = candidate_proposals[candidate]
# Hardcoded 0.75 instead of a configurable sentiment_sensitivity
# because modifying sentiment_sensitivity without changing the
# hardcoded cutoff value of 0.5 may cause unintended behaviour.
# Also, 0.75 is a reasonable number in this case.
cutoff = 0.75 * np.max(list(candidate_proposals.values()))
if cutoff < .5:
cutoff = .5
if affinity > cutoff:
new_voted_proposals[candidate] = affinity
return new_voted_proposals
def stake_across_all_supported_proposals(self, supported_proposals: List[Tuple[float, Proposal]]) -> dict:
"""
Rebalances the Participant's tokens across the (possibly updated) list of Proposals
supported by this Participant.
These tokens can come from a Participant's vesting and nonvesting TokenBatches.
"""
tokens_per_supported_proposal = {}
supported_proposals = sorted(
supported_proposals, key=lambda tup: tup[0])
total_tokens = 0
if self.holdings_vesting:
total_tokens += self.holdings_vesting.value
if self.holdings_nonvesting:
total_tokens += self.holdings_nonvesting.value
affinity_total = sum([a for a, p in supported_proposals])
for affinity, proposal in supported_proposals:
tokens_per_supported_proposal[proposal.uuid] = total_tokens * (
affinity/affinity_total)
return tokens_per_supported_proposal